/** * Team Calendar Page * Shows presence and parking for all team members * Filtered by manager (manager-centric model) */ let currentUser = null; let currentStartDate = null; let viewMode = 'week'; // 'week' or 'month' let managers = []; let teamData = []; let parkingDataLookup = {}; let parkingAssignmentLookup = {}; let selectedUserId = null; let selectedDate = null; let currentAssignmentId = null; document.addEventListener('DOMContentLoaded', async () => { if (!api.requireAuth()) return; currentUser = await api.getCurrentUser(); if (!currentUser) return; // Only managers and admins can view if (currentUser.role === 'employee') { window.location.href = '/presence'; return; } // Initialize start date based on week start preference const weekStartDay = currentUser.week_start_day || 0; currentStartDate = utils.getWeekStart(new Date(), weekStartDay); await loadManagers(); await loadTeamData(); renderCalendar(); setupEventListeners(); }); async function loadManagers() { const response = await api.get('/api/offices/managers/list'); if (response && response.ok) { managers = await response.json(); const select = document.getElementById('managerFilter'); // Filter managers based on user role let filteredManagers = managers; if (currentUser.role === 'manager') { // Manager only sees themselves filteredManagers = managers.filter(m => m.id === currentUser.id); } filteredManagers.forEach(manager => { const option = document.createElement('option'); option.value = manager.id; const officeNames = manager.offices.map(o => o.name).join(', '); option.textContent = `${manager.name} (${officeNames})`; select.appendChild(option); }); // Auto-select if only one manager (for manager role) if (filteredManagers.length === 1) { select.value = filteredManagers[0].id; } } } 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() { 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 managerFilter = document.getElementById('managerFilter').value; if (managerFilter) { url += `&manager_id=${managerFilter}`; } 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; }); } }); } } 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 = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; let headerHtml = 'NameOffice'; 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 = date.toDateString() === new Date().toDateString(); let classes = []; if (isWeekend) classes.push('weekend'); if (isHoliday) classes.push('holiday'); if (isToday) classes.push('today'); headerHtml += `
${dayNames[dayOfWeek].charAt(0)}
${date.getDate()}
`; } header.innerHTML = headerHtml; // Build body rows if (teamData.length === 0) { body.innerHTML = `No team members found`; 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 = date.toDateString() === new Date().toDateString(); 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}`); // 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 body.querySelectorAll('.calendar-cell').forEach(cell => { cell.style.cursor = 'pointer'; 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; const modal = document.getElementById('dayModal'); document.getElementById('dayModalTitle').textContent = utils.formatDateDisplay(dateStr); document.getElementById('dayModalUser').textContent = userName; // 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]; // Highlight current status document.querySelectorAll('#dayModal .status-btn').forEach(btn => { const status = btn.dataset.status; if (presence && presence.status === status) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // Update parking section const parkingSection = document.getElementById('parkingSection'); const parkingInfo = document.getElementById('parkingInfo'); if (parkingSpot) { parkingSection.style.display = 'block'; parkingInfo.innerHTML = `Parking: Spot ${parkingSpot}`; currentAssignmentId = assignmentId; } else { parkingSection.style.display = 'none'; currentAssignmentId = null; } modal.style.display = 'flex'; } async function markPresence(status) { if (!selectedUserId || !selectedDate) return; const response = await api.post('/api/presence/admin/mark', { user_id: selectedUserId, date: selectedDate, status: status }); if (response && response.ok) { document.getElementById('dayModal').style.display = 'none'; await loadTeamData(); renderCalendar(); } else { const error = await response.json(); alert(error.detail || 'Failed to mark presence'); } } async function clearPresence() { if (!selectedUserId || !selectedDate) return; if (!confirm('Clear presence for this date?')) return; const response = await api.delete(`/api/presence/admin/${selectedUserId}/${selectedDate}`); if (response && response.ok) { document.getElementById('dayModal').style.display = 'none'; await loadTeamData(); renderCalendar(); } } async function openReassignModal() { if (!currentAssignmentId) return; // Load eligible users const response = await api.get(`/api/parking/eligible-users/${currentAssignmentId}`); if (!response || !response.ok) { const error = await response.json(); alert(error.detail || 'Failed to load eligible users'); return; } const users = await response.json(); const select = document.getElementById('reassignUser'); select.innerHTML = ''; if (users.length === 0) { select.innerHTML = ''; } else { users.forEach(user => { const option = document.createElement('option'); option.value = user.id; option.textContent = user.name; select.appendChild(option); }); } // Get spot info const parkingKey = `${selectedUserId}_${selectedDate}`; const spotName = parkingDataLookup[parkingKey] || 'Unknown'; document.getElementById('reassignSpotInfo').textContent = `Spot ${spotName}`; document.getElementById('dayModal').style.display = 'none'; document.getElementById('reassignModal').style.display = 'flex'; } async function confirmReassign() { const newUserId = document.getElementById('reassignUser').value; if (!currentAssignmentId || !newUserId) { alert('Please select a user'); return; } const response = await api.post('/api/parking/reassign-spot', { assignment_id: currentAssignmentId, new_user_id: newUserId }); if (response && response.ok) { await loadTeamData(); renderCalendar(); document.getElementById('reassignModal').style.display = 'none'; } else { const error = await response.json(); alert(error.detail || 'Failed to reassign parking spot'); } } 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 || 0; currentStartDate = utils.getWeekStart(new Date(), weekStartDay); } await loadTeamData(); renderCalendar(); }); // Manager filter document.getElementById('managerFilter').addEventListener('change', async () => { await loadTeamData(); renderCalendar(); }); // Day modal document.getElementById('closeDayModal').addEventListener('click', () => { document.getElementById('dayModal').style.display = 'none'; }); document.querySelectorAll('#dayModal .status-btn').forEach(btn => { btn.addEventListener('click', () => markPresence(btn.dataset.status)); }); document.getElementById('clearDayBtn').addEventListener('click', clearPresence); document.getElementById('reassignParkingBtn').addEventListener('click', openReassignModal); utils.setupModalClose('dayModal'); // Reassign modal document.getElementById('closeReassignModal').addEventListener('click', () => { document.getElementById('reassignModal').style.display = 'none'; }); document.getElementById('cancelReassign').addEventListener('click', () => { document.getElementById('reassignModal').style.display = 'none'; }); document.getElementById('confirmReassign').addEventListener('click', confirmReassign); utils.setupModalClose('reassignModal'); }