/** * My Presence Page * Personal calendar for marking daily presence */ let currentUser = null; let currentDate = new Date(); let presenceData = {}; let parkingData = {}; let currentAssignmentId = null; document.addEventListener('DOMContentLoaded', async () => { currentUser = await api.requireAuth(); if (!currentUser) return; await Promise.all([loadPresences(), loadParkingAssignments()]); renderCalendar(); setupEventListeners(); }); async function loadPresences() { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDate = utils.formatDate(firstDay); const endDate = utils.formatDate(lastDay); const response = await api.get(`/api/presence/my-presences?start_date=${startDate}&end_date=${endDate}`); if (response && response.ok) { const presences = await response.json(); presenceData = {}; presences.forEach(p => { presenceData[p.date] = p; }); } } async function loadParkingAssignments() { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDate = utils.formatDate(firstDay); const endDate = utils.formatDate(lastDay); const response = await api.get(`/api/parking/my-assignments?start_date=${startDate}&end_date=${endDate}`); if (response && response.ok) { const assignments = await response.json(); parkingData = {}; assignments.forEach(a => { parkingData[a.date] = a; }); } } function renderCalendar() { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const weekStartDay = currentUser.week_start_day || 0; // 0=Sunday, 1=Monday // Update month header document.getElementById('currentMonth').textContent = `${utils.getMonthName(month)} ${year}`; // Get calendar info const daysInMonth = utils.getDaysInMonth(year, month); const firstDayOfMonth = new Date(year, month, 1).getDay(); // 0=Sunday const today = new Date(); // Calculate offset based on week start preference let firstDayOffset = firstDayOfMonth - weekStartDay; if (firstDayOffset < 0) firstDayOffset += 7; // Build calendar grid const grid = document.getElementById('calendarGrid'); grid.innerHTML = ''; // Day headers - reorder based on week start day const allDayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const dayNames = []; for (let i = 0; i < 7; i++) { dayNames.push(allDayNames[(weekStartDay + i) % 7]); } dayNames.forEach(name => { const header = document.createElement('div'); header.className = 'calendar-day'; header.style.cursor = 'default'; header.style.fontWeight = '600'; header.style.fontSize = '0.75rem'; header.textContent = name; grid.appendChild(header); }); // Empty cells before first day for (let i = 0; i < firstDayOffset; i++) { const empty = document.createElement('div'); empty.className = 'calendar-day'; empty.style.visibility = 'hidden'; grid.appendChild(empty); } // Day cells for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dateStr = utils.formatDate(date); const dayOfWeek = date.getDay(); const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; const isHoliday = utils.isItalianHoliday(date); const isToday = date.toDateString() === today.toDateString(); const presence = presenceData[dateStr]; const parking = parkingData[dateStr]; const cell = document.createElement('div'); cell.className = 'calendar-day'; cell.dataset.date = dateStr; if (isWeekend) cell.classList.add('weekend'); if (isHoliday) cell.classList.add('holiday'); if (isToday) cell.classList.add('today'); if (presence) { cell.classList.add(`status-${presence.status}`); } // Show parking badge if assigned const parkingBadge = parking ? `${parking.spot_display_name || parking.spot_id}` : ''; cell.innerHTML = `
${day}
${parkingBadge} `; cell.addEventListener('click', () => openDayModal(dateStr, presence, parking)); grid.appendChild(cell); } } function openDayModal(dateStr, presence, parking) { const modal = document.getElementById('dayModal'); const title = document.getElementById('dayModalTitle'); title.textContent = utils.formatDateDisplay(dateStr); // Highlight current status document.querySelectorAll('.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'); const releaseBtn = document.getElementById('releaseParkingBtn'); if (parking) { parkingSection.style.display = 'block'; const spotName = parking.spot_display_name || parking.spot_id; parkingInfo.innerHTML = `Parking: Spot ${spotName}`; releaseBtn.dataset.assignmentId = parking.id; document.getElementById('reassignParkingBtn').dataset.assignmentId = parking.id; currentAssignmentId = parking.id; } else { parkingSection.style.display = 'none'; } modal.dataset.date = dateStr; modal.style.display = 'flex'; } async function markPresence(status) { const modal = document.getElementById('dayModal'); const date = modal.dataset.date; const response = await api.post('/api/presence/mark', { date, status }); if (response && response.ok) { await Promise.all([loadPresences(), loadParkingAssignments()]); renderCalendar(); modal.style.display = 'none'; } else { const error = await response.json(); alert(error.detail || 'Failed to mark presence'); } } async function clearPresence() { const modal = document.getElementById('dayModal'); const date = modal.dataset.date; if (!confirm('Clear presence for this date?')) return; const response = await api.delete(`/api/presence/${date}`); if (response && response.ok) { await Promise.all([loadPresences(), loadParkingAssignments()]); renderCalendar(); modal.style.display = 'none'; } } async function releaseParking() { const modal = document.getElementById('dayModal'); const releaseBtn = document.getElementById('releaseParkingBtn'); const assignmentId = releaseBtn.dataset.assignmentId; if (!assignmentId) return; if (!confirm('Release your parking spot for this date?')) return; const response = await api.post(`/api/parking/release-my-spot/${assignmentId}`); if (response && response.ok) { await loadParkingAssignments(); renderCalendar(); modal.style.display = 'none'; } else { const error = await response.json(); alert(error.detail || 'Failed to release parking spot'); } } async function openReassignModal() { const assignmentId = currentAssignmentId; if (!assignmentId) return; // Load eligible users const response = await api.get(`/api/parking/eligible-users/${assignmentId}`); 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 from parking data const parking = Object.values(parkingData).find(p => p.id === assignmentId); if (parking) { const spotName = parking.spot_display_name || parking.spot_id; document.getElementById('reassignSpotInfo').textContent = `Spot ${spotName}`; } document.getElementById('dayModal').style.display = 'none'; document.getElementById('reassignModal').style.display = 'flex'; } async function confirmReassign() { const assignmentId = currentAssignmentId; const newUserId = document.getElementById('reassignUser').value; if (!assignmentId || !newUserId) { alert('Please select a user'); return; } const response = await api.post('/api/parking/reassign-spot', { assignment_id: assignmentId, new_user_id: newUserId }); if (response && response.ok) { await loadParkingAssignments(); renderCalendar(); document.getElementById('reassignModal').style.display = 'none'; } else { const error = await response.json(); alert(error.detail || 'Failed to reassign parking spot'); } } function setupEventListeners() { // Month navigation document.getElementById('prevMonth').addEventListener('click', async () => { currentDate.setMonth(currentDate.getMonth() - 1); await Promise.all([loadPresences(), loadParkingAssignments()]); renderCalendar(); }); document.getElementById('nextMonth').addEventListener('click', async () => { currentDate.setMonth(currentDate.getMonth() + 1); await Promise.all([loadPresences(), loadParkingAssignments()]); renderCalendar(); }); // Day modal document.getElementById('closeDayModal').addEventListener('click', () => { document.getElementById('dayModal').style.display = 'none'; }); document.querySelectorAll('.status-btn').forEach(btn => { btn.addEventListener('click', () => markPresence(btn.dataset.status)); }); document.getElementById('clearDayBtn').addEventListener('click', clearPresence); document.getElementById('releaseParkingBtn').addEventListener('click', releaseParking); 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'); // Bulk mark document.getElementById('bulkMarkBtn').addEventListener('click', () => { document.getElementById('bulkMarkModal').style.display = 'flex'; }); document.getElementById('closeBulkModal').addEventListener('click', () => { document.getElementById('bulkMarkModal').style.display = 'none'; }); document.getElementById('cancelBulk').addEventListener('click', () => { document.getElementById('bulkMarkModal').style.display = 'none'; }); utils.setupModalClose('bulkMarkModal'); document.getElementById('bulkMarkForm').addEventListener('submit', async (e) => { e.preventDefault(); const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const status = document.getElementById('bulkStatus').value; const weekdaysOnly = document.getElementById('weekdaysOnly').checked; const data = { start_date: startDate, end_date: endDate, status }; if (weekdaysOnly) { data.days = [1, 2, 3, 4, 5]; // Mon-Fri (JS weekday) } const response = await api.post('/api/presence/mark-bulk', data); if (response && response.ok) { const results = await response.json(); alert(`Marked ${results.length} dates`); document.getElementById('bulkMarkModal').style.display = 'none'; await Promise.all([loadPresences(), loadParkingAssignments()]); renderCalendar(); } else { const error = await response.json(); alert(error.detail || 'Failed to bulk mark'); } }); }