/** * Team Rules Page * Manage closing days, guarantees, and exclusions * Office-centric model */ let currentUser = null; let offices = []; let currentOfficeId = null; let currentOffice = null; let officeUsers = []; let currentWeeklyClosingDays = []; document.addEventListener('DOMContentLoaded', async () => { currentUser = await api.requireAuth(); if (!currentUser) return; // Only admins and managers can access this page if (currentUser.role !== 'admin' && currentUser.role !== 'manager') { window.location.href = '/presence'; return; } populateHourSelect(); await loadOffices(); setupEventListeners(); }); async function loadOffices() { const select = document.getElementById('officeSelect'); const card = document.getElementById('officeSelectionCard'); // Only Admins can see the office selector if (currentUser.role !== 'admin') { if (card) card.style.display = 'none'; } const response = await api.get('/api/offices'); if (response && response.ok) { offices = await response.json(); let filteredOffices = offices; if (currentUser.role === 'manager') { // Manager only sees their own office if (currentUser.office_id) { filteredOffices = offices.filter(o => o.id === currentUser.office_id); } else { filteredOffices = []; } } filteredOffices.forEach(office => { const option = document.createElement('option'); option.value = office.id; option.textContent = office.name; select.appendChild(option); }); // Auto-select for managers if (currentUser.role === 'manager' && filteredOffices.length === 1) { select.value = filteredOffices[0].id; loadOfficeRules(filteredOffices[0].id); } } } async function loadOfficeRules(officeId) { if (!officeId) { document.getElementById('rulesContent').style.display = 'none'; document.getElementById('noOfficeMessage').style.display = 'block'; return; } currentOfficeId = officeId; document.getElementById('rulesContent').style.display = 'block'; document.getElementById('noOfficeMessage').style.display = 'none'; // Load full office object for algorithm settings try { const response = await api.get(`/api/offices/${officeId}`); if (response && response.ok) { currentOffice = await response.json(); // Populate algorithm form const modeSelect = document.getElementById('assignmentModeSelect'); if (currentOffice.booking_window_enabled === false) { modeSelect.value = 'realtime'; } else { modeSelect.value = currentOffice.assignment_mode || 'random'; } document.getElementById('bookingWindowHour').value = currentOffice.booking_window_end_hour ?? 18; document.getElementById('bookingWindowMinute').value = currentOffice.booking_window_end_minute ?? 0; updateAlgorithmVisibility(); } } catch (e) { console.error("Error loading office details:", e); } // Load users for this office (for dropdowns) await loadOfficeUsers(officeId); await Promise.all([ loadWeeklyClosingDays(officeId), loadClosingDays(officeId), loadGuarantees(officeId), loadExclusions(officeId) ]); } async function loadOfficeUsers(officeId) { const response = await api.get(`/api/offices/${officeId}/users`); if (response && response.ok) { officeUsers = await response.json(); } } function populateHourSelect() { const hourSelect = document.getElementById('bookingWindowHour'); const minuteSelect = document.getElementById('bookingWindowMinute'); if (!hourSelect || !minuteSelect) return; hourSelect.innerHTML = ''; for (let h = 0; h < 24; h++) { const option = document.createElement('option'); option.value = h; option.textContent = h.toString().padStart(2, '0'); hourSelect.appendChild(option); } minuteSelect.innerHTML = ''; for (let m = 0; m < 60; m++) { const option = document.createElement('option'); option.value = m; option.textContent = m.toString().padStart(2, '0'); minuteSelect.appendChild(option); } } function updateAlgorithmVisibility() { const mode = document.getElementById('assignmentModeSelect').value; const group = document.getElementById('cutoffTimeGroup'); if (group) group.style.display = (mode === 'realtime') ? 'none' : 'block'; } async function saveAlgorithmSettings(e) { e.preventDefault(); if (!currentOfficeId) return; const btn = e.target.querySelector('button[type="submit"]'); const originalText = btn.textContent; btn.disabled = true; btn.textContent = 'Salvataggio...'; const mode = document.getElementById('assignmentModeSelect').value; const data = { assignment_mode: mode === 'realtime' ? 'random' : mode, booking_window_enabled: mode !== 'realtime', booking_window_end_hour: parseInt(document.getElementById('bookingWindowHour').value), booking_window_end_minute: parseInt(document.getElementById('bookingWindowMinute').value) }; try { const res = await api.put(`/api/offices/${currentOfficeId}`, data); if (res) { utils.showMessage('Impostazioni algoritmo salvate', 'success'); currentOffice = res; } } catch (e) { console.error(e); utils.showMessage('Errore nel salvataggio impostazioni', 'error'); } finally { btn.disabled = false; btn.textContent = originalText; } } // Weekly Closing Days async function loadWeeklyClosingDays(officeId) { const response = await api.get(`/api/offices/${officeId}/weekly-closing-days`); if (response && response.ok) { const days = await response.json(); currentWeeklyClosingDays = days; const activeWeekdays = days.map(d => d.weekday); document.querySelectorAll('#weeklyClosingDays input[type="checkbox"]').forEach(cb => { const weekday = parseInt(cb.dataset.weekday); cb.checked = activeWeekdays.includes(weekday); }); } } async function saveWeeklyClosingDays() { const btn = document.getElementById('saveWeeklyClosingDaysBtn'); if (!btn) return; const originalText = btn.textContent; btn.disabled = true; btn.textContent = 'Salvataggio...'; try { const promises = []; const checkboxes = document.querySelectorAll('#weeklyClosingDays input[type="checkbox"]'); for (const cb of checkboxes) { const weekday = parseInt(cb.dataset.weekday); const isChecked = cb.checked; const existingEntry = currentWeeklyClosingDays.find(d => d.weekday === weekday); if (isChecked && !existingEntry) { // Add promises.push(api.post(`/api/offices/${currentOfficeId}/weekly-closing-days`, { weekday })); } else if (!isChecked && existingEntry) { // Remove promises.push(api.delete(`/api/offices/${currentOfficeId}/weekly-closing-days/${existingEntry.id}`)); } } await Promise.all(promises); utils.showMessage('Giorni di chiusura aggiornati', 'success'); api.invalidateCache(`/api/offices/${currentOfficeId}/weekly-closing-days`); await loadWeeklyClosingDays(currentOfficeId); } catch (error) { console.error(error); utils.showMessage('Errore durante il salvataggio', 'error'); } finally { btn.disabled = false; btn.textContent = originalText; } } // Closing Days async function loadClosingDays(officeId) { const response = await api.get(`/api/offices/${officeId}/closing-days`); const container = document.getElementById('closingDaysList'); if (response && response.ok) { const days = await response.json(); if (days.length === 0) { container.innerHTML = '

Nessun giorno di chiusura specifico.

'; return; } container.innerHTML = days.map(day => `
${utils.formatDateDisplay(day.date)}${day.end_date ? ' - ' + utils.formatDateDisplay(day.end_date) : ''} ${day.reason ? `${day.reason}` : ''}
`).join(''); } } async function addClosingDay(data) { const response = await api.post(`/api/offices/${currentOfficeId}/closing-days`, data); if (response && response.ok) { api.invalidateCache(`/api/offices/${currentOfficeId}/closing-days`); await loadClosingDays(currentOfficeId); document.getElementById('closingDayModal').style.display = 'none'; document.getElementById('closingDayForm').reset(); } else { const error = await response.json(); alert(error.detail || 'Impossibile aggiungere il giorno di chiusura'); } } async function deleteClosingDay(id) { if (!confirm('Eliminare questo giorno di chiusura?')) return; const response = await api.delete(`/api/offices/${currentOfficeId}/closing-days/${id}`); if (response && response.ok) { api.invalidateCache(`/api/offices/${currentOfficeId}/closing-days`); await loadClosingDays(currentOfficeId); } } // Guarantees async function loadGuarantees(officeId) { const response = await api.get(`/api/offices/${officeId}/guarantees`); const container = document.getElementById('guaranteesList'); if (response && response.ok) { const guarantees = await response.json(); if (guarantees.length === 0) { container.innerHTML = '

Nessuna garanzia di parcheggio attiva.

'; return; } container.innerHTML = guarantees.map(g => `
${g.user_name || 'Utente sconosciuto'} ${g.start_date ? 'Dal ' + utils.formatDateDisplay(g.start_date) : 'Da sempre'} ${g.end_date ? ' al ' + utils.formatDateDisplay(g.end_date) : ''} ${g.notes ? `${g.notes}` : ''}
`).join(''); } } async function addGuarantee(data) { const response = await api.post(`/api/offices/${currentOfficeId}/guarantees`, data); if (response && response.ok) { await loadGuarantees(currentOfficeId); document.getElementById('guaranteeModal').style.display = 'none'; document.getElementById('guaranteeForm').reset(); } else { const error = await response.json(); alert(error.detail || 'Impossibile aggiungere la garanzia'); } } async function deleteGuarantee(id) { if (!confirm('Eliminare questa garanzia?')) return; const response = await api.delete(`/api/offices/${currentOfficeId}/guarantees/${id}`); if (response && response.ok) { await loadGuarantees(currentOfficeId); } } // Exclusions async function loadExclusions(officeId) { const response = await api.get(`/api/offices/${officeId}/exclusions`); const container = document.getElementById('exclusionsList'); if (response && response.ok) { const exclusions = await response.json(); if (exclusions.length === 0) { container.innerHTML = '

Nessuna esclusione attiva.

'; return; } container.innerHTML = exclusions.map(e => `
${e.user_name || 'Utente sconosciuto'} ${e.start_date ? 'Dal ' + utils.formatDateDisplay(e.start_date) : 'Da sempre'} ${e.end_date ? ' al ' + utils.formatDateDisplay(e.end_date) : ''} ${e.notes ? `${e.notes}` : ''}
`).join(''); } } // Global variable to track edit mode let editingExclusionId = null; async function openEditExclusion(id, data) { editingExclusionId = id; // Populate form populateUserSelects(); document.getElementById('exclusionUser').value = data.user_id; // Disable user select in edit mode usually? Or allow change? API allows it. document.getElementById('exclusionStartDate').value = data.start_date || ''; document.getElementById('exclusionEndDate').value = data.end_date || ''; document.getElementById('exclusionNotes').value = data.notes || ''; // Change modal title/button document.querySelector('#exclusionModal h3').textContent = 'Modifica Esclusione'; document.querySelector('#exclusionForm button[type="submit"]').textContent = 'Salva Modifiche'; document.getElementById('exclusionModal').style.display = 'flex'; } async function saveExclusion(data) { let response; if (editingExclusionId) { response = await api.put(`/api/offices/${currentOfficeId}/exclusions/${editingExclusionId}`, data); } else { response = await api.post(`/api/offices/${currentOfficeId}/exclusions`, data); } if (response && response.ok) { await loadExclusions(currentOfficeId); document.getElementById('exclusionModal').style.display = 'none'; resetExclusionForm(); } else { const error = await response.json(); alert(error.detail || 'Impossibile salvare l\'esclusione'); } } function resetExclusionForm() { document.getElementById('exclusionForm').reset(); editingExclusionId = null; document.querySelector('#exclusionModal h3').textContent = 'Aggiungi Esclusione Parcheggio'; document.querySelector('#exclusionForm button[type="submit"]').textContent = 'Aggiungi'; } async function deleteExclusion(id) { if (!confirm('Eliminare questa esclusione?')) return; const response = await api.delete(`/api/offices/${currentOfficeId}/exclusions/${id}`); if (response && response.ok) { await loadExclusions(currentOfficeId); } } function populateUserSelects() { const selects = ['guaranteeUser', 'exclusionUser']; selects.forEach(id => { const select = document.getElementById(id); const currentVal = select.value; select.innerHTML = ''; officeUsers.forEach(user => { const option = document.createElement('option'); option.value = user.id; option.textContent = user.name; select.appendChild(option); }); if (currentVal) select.value = currentVal; }); } function setupEventListeners() { // Office select document.getElementById('officeSelect').addEventListener('change', (e) => { loadOfficeRules(e.target.value); }); // Save Weekly closing days const saveBtn = document.getElementById('saveWeeklyClosingDaysBtn'); if (saveBtn) { saveBtn.addEventListener('click', saveWeeklyClosingDays); } // Modals const modals = [ { id: 'closingDayModal', btn: 'addClosingDayBtn', close: 'closeClosingDayModal', cancel: 'cancelClosingDay' }, { id: 'guaranteeModal', btn: 'addGuaranteeBtn', close: 'closeGuaranteeModal', cancel: 'cancelGuarantee' }, { id: 'exclusionModal', btn: 'addExclusionBtn', close: 'closeExclusionModal', cancel: 'cancelExclusion' } ]; modals.forEach(m => { document.getElementById(m.btn).addEventListener('click', () => { if (m.id !== 'closingDayModal') populateUserSelects(); // Special handling for exclusion to reset edit mode if (m.id === 'exclusionModal') resetExclusionForm(); document.getElementById(m.id).style.display = 'flex'; }); document.getElementById(m.close).addEventListener('click', () => { document.getElementById(m.id).style.display = 'none'; }); document.getElementById(m.cancel).addEventListener('click', () => { document.getElementById(m.id).style.display = 'none'; }); utils.setupModalClose(m.id); }); // Forms document.getElementById('closingDayForm').addEventListener('submit', (e) => { e.preventDefault(); addClosingDay({ date: document.getElementById('closingDate').value, end_date: document.getElementById('closingEndDate').value || null, reason: document.getElementById('closingReason').value || null }); }); document.getElementById('guaranteeForm').addEventListener('submit', (e) => { e.preventDefault(); addGuarantee({ user_id: document.getElementById('guaranteeUser').value, start_date: document.getElementById('guaranteeStartDate').value || null, end_date: document.getElementById('guaranteeEndDate').value || null, notes: document.getElementById('guaranteeNotes').value || null }); }); document.getElementById('exclusionForm').addEventListener('submit', (e) => { e.preventDefault(); saveExclusion({ user_id: document.getElementById('exclusionUser').value, start_date: document.getElementById('exclusionStartDate').value || null, end_date: document.getElementById('exclusionEndDate').value || null, notes: document.getElementById('exclusionNotes').value || null }); }); // Algorithm settings events document.getElementById('assignmentModeSelect').addEventListener('change', updateAlgorithmVisibility); document.getElementById('algorithmForm').addEventListener('submit', saveAlgorithmSettings); // Test Tools Logic // Set default date to tomorrow const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const testDateStart = document.getElementById('testDateStart'); if (testDateStart) testDateStart.valueAsDate = tomorrow; document.getElementById('runAllocationBtn').addEventListener('click', async () => { if (!confirm('Sei sicuro di voler avviare l\'assegnazione ORA? Questo potrebbe sovrascrivere le assegnazioni esistenti per la data selezionata.')) return; const dateStart = document.getElementById('testDateStart').value; const dateEnd = document.getElementById('testDateEnd').value; if (!dateStart) return utils.showMessage('Seleziona una data di inizio', 'error'); let start = new Date(dateStart); let end = dateEnd ? new Date(dateEnd) : new Date(dateStart); if (end < start) { return utils.showMessage('La data di fine deve essere successiva alla data di inizio', 'error'); } let current = new Date(start); let successCount = 0; let errorCount = 0; utils.showMessage('Avvio assegnazione...', 'success'); while (current <= end) { const dateStr = utils.formatDate(current); try { await api.post('/api/parking/run-allocation', { date: dateStr, office_id: currentOfficeId }); successCount++; } catch (e) { console.error(`Error for ${dateStr}`, e); errorCount++; } current.setDate(current.getDate() + 1); } if (errorCount === 0) { utils.showMessage(`Assegnazione completata per ${successCount} giorni.`, 'success'); } else { utils.showMessage(`Completato con errori: ${successCount} successi, ${errorCount} errori.`, 'warning'); } }); document.getElementById('clearAssignmentsBtn').addEventListener('click', async () => { if (!confirm('ATTENZIONE: Stai per eliminare TUTTE le assegnazioni per il periodo selezionato. Procedere?')) return; const dateStart = document.getElementById('testDateStart').value; const dateEnd = document.getElementById('testDateEnd').value; if (!dateStart) return utils.showMessage('Seleziona una data di inizio', 'error'); let start = new Date(dateStart); let end = dateEnd ? new Date(dateEnd) : new Date(dateStart); if (end < start) { return utils.showMessage('La data di fine deve essere successiva alla data di inizio', 'error'); } let current = new Date(start); let totalRemoved = 0; utils.showMessage('Rimozione in corso...', 'warning'); while (current <= end) { const dateStr = utils.formatDate(current); try { const res = await api.post('/api/parking/clear-assignments', { date: dateStr, office_id: currentOfficeId }); if (res && res.ok) { const data = await res.json(); totalRemoved += (data.count || 0); } } catch (e) { console.error(`Error clearing ${dateStr}`, e); } current.setDate(current.getDate() + 1); } utils.showMessage(`Operazione eseguita.`, 'warning'); }); const clearPresenceBtn = document.getElementById('clearPresenceBtn'); if (clearPresenceBtn) { clearPresenceBtn.addEventListener('click', async () => { if (!confirm('ATTENZIONE: Stai per eliminare TUTTI GLI STATI (Presente/Assente/ecc) e relative assegnazioni per tutti gli utenti del gruppo nel periodo selezionato. \n\nQuesta azione è irreversibile. Procedere?')) return; const dateStart = document.getElementById('testDateStart').value; const dateEnd = document.getElementById('testDateEnd').value; if (!dateStart) return utils.showMessage('Seleziona una data di inizio', 'error'); // Validate office if (!currentOfficeId) { return utils.showMessage('Errore: Nessun gruppo selezionato', 'error'); } const endDateVal = dateEnd || dateStart; utils.showMessage('Rimozione stati in corso...', 'warning'); try { const res = await api.post('/api/presence/admin/clear-office-presence', { start_date: dateStart, end_date: endDateVal, office_id: currentOfficeId }); if (res && res.ok) { const data = await res.json(); utils.showMessage(`Operazione completata. Rimossi ${data.count_presence} stati e ${data.count_parking} assegnazioni.`, 'success'); } else { const err = await res.json(); utils.showMessage('Errore: ' + (err.detail || 'Operazione fallita'), 'error'); } } catch (e) { console.error(e); utils.showMessage('Errore di comunicazione col server', 'error'); } }); } const testEmailBtn = document.getElementById('testEmailBtn'); if (testEmailBtn) { testEmailBtn.addEventListener('click', async () => { const dateVal = document.getElementById('testEmailDate').value; // Validate office if (!currentOfficeId) { return utils.showMessage('Errore: Nessun gruppo selezionato', 'error'); } utils.showMessage('Invio mail di test in corso...', 'warning'); try { const res = await api.post('/api/parking/test-email', { date: dateVal || null, office_id: currentOfficeId }); if (res && res.status >= 200 && res.status < 300) { const data = await res.json(); if (data.success) { let msg = `Email inviata con successo per la data: ${data.date}.`; if (data.mode === 'FILE') { msg += ' (SMTP disabilitato: Loggato su file)'; } utils.showMessage(msg, 'success'); } else { utils.showMessage('Invio fallito. Controlla i log del server.', 'error'); } } else { const err = res ? await res.json() : {}; console.error("Test Email Error:", err); const errMsg = err.detail ? (typeof err.detail === 'object' ? JSON.stringify(err.detail) : err.detail) : 'Invio fallito'; utils.showMessage('Errore: ' + errMsg, 'error'); } } catch (e) { console.error(e); utils.showMessage('Errore di comunicazione col server', 'error'); } }); } const bulkEmailBtn = document.getElementById('bulkEmailBtn'); if (bulkEmailBtn) { bulkEmailBtn.addEventListener('click', async () => { const dateVal = document.getElementById('testEmailDate').value; // Validate office if (!currentOfficeId) { return utils.showMessage('Errore: Nessun gruppo selezionato', 'error'); } if (!dateVal) { return utils.showMessage('Per il test a TUTTI è obbligatorio selezionare una data specifica.', 'error'); } if (!confirm(`Sei sicuro di voler inviare una mail di promemoria a TUTTI gli utenti con parcheggio assegnato per il giorno ${dateVal}?\n\nQuesta azione invierà vere email.`)) return; utils.showMessage('Invio mail massive in corso...', 'warning'); try { const res = await api.post('/api/parking/test-email', { date: dateVal, office_id: currentOfficeId, bulk_send: true }); if (res && res.status >= 200 && res.status < 300) { const data = await res.json(); if (data.success) { let msg = `Processo completato per il ${data.date}. Inviate: ${data.count || 0}, Fallite: ${data.failed || 0}.`; if (data.mode === 'BULK' && (data.count || 0) === 0) msg += " (Nessun assegnatario trovato)"; utils.showMessage(msg, 'success'); } else { utils.showMessage('Errore durante l\'invio.', 'error'); } } else { const err = res ? await res.json() : {}; console.error("Bulk Test Email Error:", err); const errMsg = err.detail ? (typeof err.detail === 'object' ? JSON.stringify(err.detail) : err.detail) : 'Invio fallito'; utils.showMessage('Errore: ' + errMsg, 'error'); } } catch (e) { console.error(e); utils.showMessage('Errore di comunicazione col server: ' + e.message, 'error'); } }); } } // Global functions window.deleteClosingDay = deleteClosingDay; window.deleteGuarantee = deleteGuarantee; window.deleteClosingDay = deleteClosingDay; window.deleteGuarantee = deleteGuarantee; window.deleteExclusion = deleteExclusion; window.openEditExclusion = openEditExclusion;