Features: - Manager-centric parking spot management - Fair assignment algorithm (parking/presence ratio) - Presence tracking calendar - Closing days (specific & weekly recurring) - Guarantees and exclusions - Authelia/LLDAP integration for SSO Stack: - FastAPI backend - SQLite database - Vanilla JS frontend - Docker deployment
223 lines
5.6 KiB
JavaScript
223 lines
5.6 KiB
JavaScript
/**
|
|
* 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
|
|
};
|