/** * Team Calendar Page * Shows presence and parking for all team members * Filtered by office (office-centric model) */ let currentUser = null; let currentStartDate = null; let viewMode = 'week'; // 'week' or 'month' let offices = []; let teamData = []; let parkingDataLookup = {}; let parkingAssignmentLookup = {}; let selectedUserId = null; let selectedDate = null; let currentAssignmentId = null; let officeClosingRules = {}; // { officeId: { weekly: [], specific: [] } } document.addEventListener('DOMContentLoaded', async () => { currentUser = await api.requireAuth(); if (!currentUser) return; // Initialize start date based on week start preference const weekStartDay = currentUser.week_start_day || 1; currentStartDate = utils.getWeekStart(new Date(), weekStartDay); await loadOffices(); await loadTeamData(); await loadTeamData(); // Initialize Modal Logic ModalLogic.init({ onMarkPresence: handleMarkPresence, onClearPresence: handleClearPresence, onReleaseParking: handleReleaseParking, onReassignParking: handleReassignParking }); renderCalendar(); setupEventListeners(); }); function updateOfficeDisplay() { const display = document.getElementById('currentOfficeNameDisplay'); if (!display) return; const select = document.getElementById('officeFilter'); // If user is employee, show their office name directly if (currentUser.role === 'employee') { display.textContent = currentUser.office_name || "Mio Ufficio"; return; } // For admin/manager, show selected if (select && select.value) { // Find name in options const option = select.options[select.selectedIndex]; if (option) { // Remove the count (xx utenti) part if desired, or keep it. // User requested "nome del'ufficio", let's keep it simple. // Option text is "Name (Count users)" // let text = option.textContent.split('(')[0].trim(); display.textContent = option.textContent; } else { display.textContent = "Tutti gli Uffici"; } } else { display.textContent = "Tutti gli Uffici"; } } async function loadOffices() { const select = document.getElementById('officeFilter'); // Only Admins and Managers can list offices // Employees will just see their own office logic handled in loadTeamData // Only Admins can see the office selector if (currentUser.role !== 'admin') { select.style.display = 'none'; // Employees stop here, Managers continue to allow auto-selection logic below if (currentUser.role === 'employee') return; } 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 in the filter? // Actually managers might want to filter if they (hypothetically) managed multiple, // but currently User has 1 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} (${office.user_count || 0} utenti)`; select.appendChild(option); }); // Auto-select for managers if (currentUser.role === 'manager' && filteredOffices.length === 1) { select.value = filteredOffices[0].id; } } // Initial update of office display updateOfficeDisplay(); } function getDateRange() { let startDate = new Date(currentStartDate); let endDate = new Date(currentStartDate); if (viewMode === 'week') { endDate.setDate(endDate.getDate() + 6); } else { // Month view - start from first day of month startDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth(), 1); endDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth() + 1, 0); } return { startDate, endDate }; } async function loadTeamData() { await loadClosingData(); const { startDate, endDate } = getDateRange(); const startStr = utils.formatDate(startDate); const endStr = utils.formatDate(endDate); let url = `/api/presence/team?start_date=${startStr}&end_date=${endStr}`; const officeFilter = document.getElementById('officeFilter').value; if (officeFilter) { url += `&office_id=${officeFilter}`; } const response = await api.get(url); if (response && response.ok) { teamData = await response.json(); // Build parking lookup with spot names and assignment IDs parkingDataLookup = {}; parkingAssignmentLookup = {}; teamData.forEach(member => { if (member.parking_info) { member.parking_info.forEach(p => { const key = `${member.id}_${p.date}`; parkingDataLookup[key] = p.spot_display_name || p.spot_id; parkingAssignmentLookup[key] = p.id; }); } }); } } async function loadClosingData() { officeClosingRules = {}; let officeIdsToLoad = []; const selectedOfficeId = document.getElementById('officeFilter').value; if (selectedOfficeId) { officeIdsToLoad = [selectedOfficeId]; } else if (currentUser.role === 'employee' || (currentUser.role === 'manager' && currentUser.office_id)) { officeIdsToLoad = [currentUser.office_id]; } else if (offices.length > 0) { // Admin viewing all or Manager with access to list officeIdsToLoad = offices.map(o => o.id); } if (officeIdsToLoad.length === 0) return; // Fetch in parallel const promises = officeIdsToLoad.map(async (oid) => { try { const [weeklyRes, specificRes] = await Promise.all([ api.get(`/api/offices/${oid}/weekly-closing-days`), api.get(`/api/offices/${oid}/closing-days`) ]); officeClosingRules[oid] = { weekly: [], specific: [] }; if (weeklyRes && weeklyRes.ok) { const days = await weeklyRes.json(); officeClosingRules[oid].weekly = days.map(d => d.weekday); } if (specificRes && specificRes.ok) { officeClosingRules[oid].specific = await specificRes.json(); // OPTIMIZATION: Pre-calculate all specific closed dates into a Set const closedSet = new Set(); officeClosingRules[oid].specific.forEach(range => { let start = new Date(range.date); let end = range.end_date ? new Date(range.end_date) : new Date(range.date); // Normalize to noon to avoid timezone issues when stepping start.setHours(12, 0, 0, 0); end.setHours(12, 0, 0, 0); let current = new Date(start); while (current <= end) { closedSet.add(utils.formatDate(current)); current.setDate(current.getDate() + 1); } }); officeClosingRules[oid].closedDatesSet = closedSet; } } catch (e) { console.error(`Error loading closing days for office ${oid}:`, e); } }); await Promise.all(promises); } function renderCalendar() { const header = document.getElementById('calendarHeader'); const body = document.getElementById('calendarBody'); const { startDate, endDate } = getDateRange(); // Update header text if (viewMode === 'week') { document.getElementById('currentWeek').textContent = `${utils.formatDateShort(startDate)} - ${utils.formatDateShort(endDate)}`; } else { document.getElementById('currentWeek').textContent = `${utils.getMonthName(startDate.getMonth())} ${startDate.getFullYear()}`; } // Calculate number of days const dayCount = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1; // Build header row const dayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']; let headerHtml = 'NomeUfficio'; for (let i = 0; i < dayCount; i++) { const date = new Date(startDate); date.setDate(date.getDate() + i); const dayOfWeek = date.getDay(); const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; const isHoliday = utils.isItalianHoliday(date); const isToday = utils.formatDate(date) === utils.formatDate(new Date()); let classes = []; if (isWeekend) classes.push('weekend'); if (isHoliday) classes.push('holiday'); if (isToday) classes.push('today'); if (isToday) classes.push('today'); // Header doesn't show closed status in multi-office view // unless we want to check if ALL are closed? // For now, simpler to leave header clean. headerHtml += `
${dayNames[dayOfWeek].charAt(0)}
${date.getDate()}
`; } header.innerHTML = headerHtml; // Build body rows if (teamData.length === 0) { body.innerHTML = `Nessun membro del team trovato`; return; } let bodyHtml = ''; teamData.forEach(member => { bodyHtml += ` ${member.name || 'Unknown'} ${member.office_name || '-'}`; for (let i = 0; i < dayCount; i++) { const date = new Date(startDate); date.setDate(date.getDate() + i); const dateStr = utils.formatDate(date); const dayOfWeek = date.getDay(); const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; const isHoliday = utils.isItalianHoliday(date); const presence = member.presences.find(p => p.date === dateStr); const parkingKey = `${member.id}_${dateStr}`; const parkingSpot = parkingDataLookup[parkingKey]; const hasParking = member.parking_dates && member.parking_dates.includes(dateStr); const isToday = dateStr === utils.formatDate(new Date()); let cellClasses = ['calendar-cell']; if (isWeekend) cellClasses.push('weekend'); if (isHoliday) cellClasses.push('holiday'); if (isToday) cellClasses.push('today'); if (presence) cellClasses.push(`status-${presence.status}`); if (isToday) cellClasses.push('today'); if (presence) cellClasses.push(`status-${presence.status}`); // Optimized closing day check // Pre-calculate loop-invariant sets outside if not already done, but here we do it per-cell because of date dependency? // BETTER: We should pre-calculate a "closedMap" for the viewed range for each office? // OR: Just optimize the inner check. // Optimization: Create a lookup string for the current date once // (Already have dateStr) const memberRules = officeClosingRules[member.office_id]; let isClosed = false; if (memberRules) { // Check weekly if (memberRules.weekly.includes(dayOfWeek)) { isClosed = true; } else if (memberRules.specific && memberRules.specific.length > 0) { // Check specific // Optimization: Use the string date lookup if we had a Set, but we have ranges. // We can optimize by converting ranges to Sets ONCE when loading data, // OR just stick to this check if N is small. // Given the "optimization" task, let's just make sure we don't do new Date() inside. // The `specific` array contains objects with `date` and `end_date` strings. // We can compare strings directly if format is YYYY-MM-DD and we are careful. // Optimization: check if dateStr is in a Set of closed dates for this office? // Let's implement the Set lookup logic in `loadClosingData` or `renderCalendar` start. // For now, let's assume `memberRules.closedDatesSet` exists. if (memberRules.closedDatesSet && memberRules.closedDatesSet.has(dateStr)) { isClosed = true; } } } if (isClosed) { cellClasses.push('closed'); } // Show parking badge instead of just 'P' let parkingBadge = ''; if (hasParking) { const spotName = parkingSpot || 'P'; parkingBadge = `${spotName}`; } bodyHtml += `${parkingBadge}`; } bodyHtml += ''; }); body.innerHTML = bodyHtml; // Add click handlers to cells (only for admins and managers) if (currentUser.role === 'admin' || currentUser.role === 'manager') { body.querySelectorAll('.calendar-cell').forEach(cell => { cell.style.cursor = 'pointer'; if (cell.dataset.closed !== 'true') { cell.addEventListener('click', () => { const userId = cell.dataset.userId; const date = cell.dataset.date; const userName = cell.dataset.userName; openDayModal(userId, date, userName); }); } }); } } function openDayModal(userId, dateStr, userName) { selectedUserId = userId; selectedDate = dateStr; // Find current status and parking const member = teamData.find(m => m.id === userId); const presence = member?.presences.find(p => p.date === dateStr); const parkingKey = `${userId}_${dateStr}`; const parkingSpot = parkingDataLookup[parkingKey]; const assignmentId = parkingAssignmentLookup[parkingKey]; currentAssignmentId = assignmentId; // Ensure this is set for modal logic const parkingObj = assignmentId ? { id: assignmentId, spot_display_name: parkingSpot, spot_id: parkingSpot } : null; ModalLogic.openModal({ dateStr, userName, presence, parking: parkingObj, userId }); } async function handleMarkPresence(status, date, userId) { // userId passed from ModalLogic if provided, or use selectedUserId const targetUserId = userId || selectedUserId; if (!targetUserId) return; const response = await api.post('/api/presence/admin/mark', { user_id: targetUserId, date: date, status: status }); if (response && response.ok) { ModalLogic.closeModal(); await loadTeamData(); renderCalendar(); } else { const error = await response.json(); alert(error.detail || 'Impossibile segnare la presenza'); } } async function handleClearPresence(date, userId) { const targetUserId = userId || selectedUserId; if (!targetUserId) return; // confirm is not needed here if ModalLogic doesn't mandate it, but keeping logic // ModalLogic buttons usually trigger this directly. const response = await api.delete(`/api/presence/admin/${targetUserId}/${date}`); if (response && response.ok) { ModalLogic.closeModal(); await loadTeamData(); renderCalendar(); } } async function handleReleaseParking(assignmentId) { if (!confirm('Rilasciare il parcheggio per questa data?')) return; // Note: Admin endpoint for releasing ANY spot vs "my spot" // Since we are admin/manager here, we might need a general release endpoint or use reassign with null? // The current 'release_my_spot' is only for self. // 'reassign_spot' with null user_id is the way for admins. const response = await api.post('/api/parking/reassign-spot', { assignment_id: assignmentId, new_user_id: null // Release }); if (response && response.ok) { ModalLogic.closeModal(); await loadTeamData(); renderCalendar(); } else { const error = await response.json(); alert(error.detail || 'Impossibile rilasciare il parcheggio'); } } async function handleReassignParking(assignmentId, newUserId) { if (!assignmentId || !newUserId) { alert('Seleziona un utente'); return; } const response = await api.post('/api/parking/reassign-spot', { assignment_id: assignmentId, new_user_id: newUserId }); if (response && response.ok) { await loadTeamData(); renderCalendar(); ModalLogic.closeModal(); } else { const error = await response.json(); alert(error.detail || 'Impossibile riassegnare il parcheggio'); } } function setupEventListeners() { // Navigation (prev/next) document.getElementById('prevWeek').addEventListener('click', async () => { if (viewMode === 'week') { currentStartDate.setDate(currentStartDate.getDate() - 7); } else { currentStartDate.setMonth(currentStartDate.getMonth() - 1); } await loadTeamData(); renderCalendar(); }); document.getElementById('nextWeek').addEventListener('click', async () => { if (viewMode === 'week') { currentStartDate.setDate(currentStartDate.getDate() + 7); } else { currentStartDate.setMonth(currentStartDate.getMonth() + 1); } await loadTeamData(); renderCalendar(); }); // View toggle (week/month) document.getElementById('viewToggle').addEventListener('change', async (e) => { viewMode = e.target.value; if (viewMode === 'month') { // Set to first day of current month currentStartDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth(), 1); } else { // Set to current week start const weekStartDay = currentUser.week_start_day || 1; currentStartDate = utils.getWeekStart(new Date(), weekStartDay); } await loadTeamData(); renderCalendar(); }); // Office filter document.getElementById('officeFilter').addEventListener('change', async () => { updateOfficeDisplay(); // Update label on change await loadTeamData(); renderCalendar(); }); utils.setupModalClose('dayModal'); }