/** * Utility Functions * Date handling, holidays, and common helpers */ // Holiday cache: { year: Set of date strings } const holidayCache = {}; /** * Load holidays for a year from API (called automatically) */ async function loadHolidaysForYear(year) { if (holidayCache[year]) return; try { const response = await fetch(`/api/auth/holidays/${year}`); if (response.ok) { const holidays = await response.json(); holidayCache[year] = new Set(holidays.map(h => h.date)); } } catch (e) { // Fall back to local calculation if API fails console.warn('Holiday API failed, using local fallback'); } } /** * Calculate Easter Sunday using Computus algorithm (fallback) */ function calculateEaster(year) { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return new Date(year, month - 1, day); } /** * Check if a date is an Italian holiday * Uses cached API data if available, otherwise falls back to local calculation */ function isItalianHoliday(date) { const year = date.getFullYear(); const dateStr = formatDate(date); // Check cache first if (holidayCache[year] && holidayCache[year].has(dateStr)) { return true; } // Fallback: local calculation const month = date.getMonth() + 1; const day = date.getDate(); const fixedHolidays = [ [1, 1], [1, 6], [4, 25], [5, 1], [6, 2], [8, 15], [11, 1], [12, 8], [12, 25], [12, 26] ]; for (const [hm, hd] of fixedHolidays) { if (month === hm && day === hd) return true; } // Easter Monday const easter = calculateEaster(year); const easterMonday = new Date(easter); easterMonday.setDate(easter.getDate() + 1); if (date.getMonth() === easterMonday.getMonth() && date.getDate() === easterMonday.getDate()) { return true; } return false; } /** * Format date as YYYY-MM-DD */ function formatDate(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Format date for display */ function formatDateDisplay(dateStr) { const date = new Date(dateStr + 'T12:00:00'); return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); } /** * Get month name */ function getMonthName(month) { const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; return months[month]; } /** * Get day name */ function getDayName(dayIndex) { const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return days[dayIndex]; } /** * Get days in month */ function getDaysInMonth(year, month) { return new Date(year, month + 1, 0).getDate(); } /** * Get start of week for a date */ function getWeekStart(date, weekStartDay = 0) { const d = new Date(date); const day = d.getDay(); const diff = (day - weekStartDay + 7) % 7; d.setDate(d.getDate() - diff); d.setHours(0, 0, 0, 0); return d; } /** * Format date as short display (e.g., "Nov 26") */ function formatDateShort(date) { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } /** * Show a temporary message (creates toast if no container) */ function showMessage(message, type = 'success', duration = 3000) { // Create toast container if it doesn't exist let toastContainer = document.getElementById('toastContainer'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'toastContainer'; toastContainer.style.cssText = ` position: fixed; top: 1rem; right: 1rem; z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem; `; document.body.appendChild(toastContainer); } const toast = document.createElement('div'); toast.className = `message ${type}`; toast.style.cssText = ` padding: 0.75rem 1rem; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); animation: slideIn 0.2s ease; `; toast.textContent = message; toastContainer.appendChild(toast); if (duration > 0) { setTimeout(() => { toast.style.animation = 'slideOut 0.2s ease'; setTimeout(() => toast.remove(), 200); }, duration); } } /** * Close modal when clicking outside */ function setupModalClose(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.addEventListener('click', (e) => { if (e.target.id === modalId) { modal.style.display = 'none'; } }); } } // Export utilities window.utils = { loadHolidaysForYear, isItalianHoliday, formatDate, formatDateDisplay, formatDateShort, getMonthName, getDayName, getDaysInMonth, getWeekStart, showMessage, setupModalClose };