/**
* My Presence Page
* Personal calendar for marking daily presence
*/
let currentUser = null;
let currentDate = new Date();
let presenceData = {};
let parkingData = {};
let currentAssignmentId = null;
let weeklyClosingDays = [];
let specificClosingDays = [];
let statusDate = new Date();
let statusViewMode = 'daily';
document.addEventListener('DOMContentLoaded', async () => {
currentUser = await api.requireAuth();
if (!currentUser) return;
await Promise.all([loadPresences(), loadParkingAssignments(), loadClosingDays()]);
// Initialize Modal Logic
ModalLogic.init({
onMarkPresence: handleMarkPresence,
onClearPresence: handleClearPresence,
onReleaseParking: handleReleaseParking,
onReassignParking: handleReassignParking
});
renderCalendar();
setupEventListeners();
// Initialize Parking Status
initParkingStatus();
setupStatusListeners();
});
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;
});
}
}
async function loadClosingDays() {
if (!currentUser.office_id) return;
try {
const [weeklyRes, specificRes] = await Promise.all([
api.get(`/api/offices/${currentUser.office_id}/weekly-closing-days`),
api.get(`/api/offices/${currentUser.office_id}/closing-days`)
]);
if (weeklyRes && weeklyRes.ok) {
const days = await weeklyRes.json();
weeklyClosingDays = days.map(d => d.weekday);
}
if (specificRes && specificRes.ok) {
specificClosingDays = await specificRes.json();
}
} catch (e) {
console.error('Error loading closing days:', e);
}
}
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const weekStartDay = currentUser.week_start_day || 1; // 0=Sunday, 1=Monday (default to 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 = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
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');
// Check closing days
// Note: JS getDay(): 0=Sunday, 1=Monday...
// DB WeekDay: 0=Sunday, etc. (They match)
const isWeeklyClosed = weeklyClosingDays.includes(dayOfWeek);
const isSpecificClosed = specificClosingDays.some(d => {
const start = new Date(d.date);
const end = d.end_date ? new Date(d.end_date) : start;
// Reset times for strict date comparison
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
return date >= start && date <= end;
});
const isClosed = isWeeklyClosed || isSpecificClosed;
if (isClosed) {
cell.classList.add('closed');
cell.title = "Ufficio Chiuso";
} else 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}
`;
if (!isClosed) {
cell.addEventListener('click', () => openDayModal(dateStr, presence, parking));
}
grid.appendChild(cell);
}
}
function openDayModal(dateStr, presence, parking) {
ModalLogic.openModal({
dateStr,
presence,
parking
});
}
async function handleMarkPresence(status, date) {
const response = await api.post('/api/presence/mark', { date, status });
if (response && response.ok) {
await Promise.all([loadPresences(), loadParkingAssignments()]);
renderCalendar();
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Impossibile segnare la presenza');
}
}
async function handleClearPresence(date) {
const response = await api.delete(`/api/presence/${date}`);
if (response && response.ok) {
await Promise.all([loadPresences(), loadParkingAssignments()]);
renderCalendar();
ModalLogic.closeModal();
}
}
async function handleReleaseParking(assignmentId) {
if (!confirm('Rilasciare il parcheggio per questa data?')) return;
const response = await api.post(`/api/parking/release-my-spot/${assignmentId}`);
if (response && response.ok) {
await loadParkingAssignments();
renderCalendar();
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Impossibile rilasciare il parcheggio');
}
}
async function handleReassignParking(assignmentId, newUserId) {
// Basic validation handled by select; confirm
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 loadParkingAssignments();
renderCalendar();
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Impossibile riassegnare il parcheggio');
}
}
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();
});
// Quick Entry Logic
const quickEntryModal = document.getElementById('quickEntryModal');
const quickEntryBtn = document.getElementById('quickEntryBtn');
const closeQuickEntryBtn = document.getElementById('closeQuickEntryModal');
const cancelQuickEntryBtn = document.getElementById('cancelQuickEntry');
const quickEntryForm = document.getElementById('quickEntryForm');
if (quickEntryBtn) {
quickEntryBtn.addEventListener('click', () => {
// Default dates: tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
document.getElementById('qeStartDate').valueAsDate = tomorrow;
document.getElementById('qeEndDate').valueAsDate = tomorrow;
document.getElementById('qeStatus').value = '';
// Clear selections
document.querySelectorAll('.qe-status-btn').forEach(btn => btn.classList.remove('active'));
quickEntryModal.style.display = 'flex';
});
}
if (closeQuickEntryBtn) closeQuickEntryBtn.addEventListener('click', () => quickEntryModal.style.display = 'none');
if (cancelQuickEntryBtn) cancelQuickEntryBtn.addEventListener('click', () => quickEntryModal.style.display = 'none');
// Status selection in QE
document.querySelectorAll('.qe-status-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.qe-status-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('qeStatus').value = btn.dataset.status;
});
});
if (quickEntryForm) {
quickEntryForm.addEventListener('submit', async (e) => {
e.preventDefault();
const startStr = document.getElementById('qeStartDate').value;
const endStr = document.getElementById('qeEndDate').value;
const status = document.getElementById('qeStatus').value;
if (!status) return utils.showMessage('Seleziona uno stato', 'error');
if (!startStr || !endStr) return utils.showMessage('Seleziona le date', 'error');
const startDate = new Date(startStr);
const endDate = new Date(endStr);
if (endDate < startDate) return utils.showMessage('La data di fine non può essere precedente alla data di inizio', 'error');
quickEntryModal.style.display = 'none';
utils.showMessage('Inserimento in corso...', 'warning');
const promises = [];
let current = new Date(startDate);
while (current <= endDate) {
const dStr = current.toISOString().split('T')[0];
if (status === 'clear') {
promises.push(api.delete(`/api/presence/${dStr}`));
} else {
promises.push(api.post('/api/presence/mark', { date: dStr, status: status }));
}
current.setDate(current.getDate() + 1);
}
try {
await Promise.all(promises);
utils.showMessage('Inserimento completato!', 'success');
await Promise.all([loadPresences(), loadParkingAssignments()]);
renderCalendar();
} catch (err) {
console.error(err);
utils.showMessage('Errore durante l\'inserimento. Alcuni giorni potrebbero non essere stati aggiornati.', 'error');
}
});
}
}
// ----------------------------------------------------------------------------
// Parking Status Logic
// ----------------------------------------------------------------------------
function initParkingStatus() {
updateStatusHeader();
loadDailyStatus();
// Update office name if available
if (currentUser && currentUser.office_name) {
const nameDisplay = document.getElementById('statusOfficeName');
if (nameDisplay) nameDisplay.textContent = currentUser.office_name;
const headerDisplay = document.getElementById('currentOfficeDisplay');
if (headerDisplay) headerDisplay.textContent = currentUser.office_name;
} else {
const nameDisplay = document.getElementById('statusOfficeName');
if (nameDisplay) nameDisplay.textContent = 'Tuo Ufficio';
const headerDisplay = document.getElementById('currentOfficeDisplay');
if (headerDisplay) headerDisplay.textContent = 'Tuo Ufficio';
}
}
function updateStatusHeader() {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const dateStr = statusDate.toLocaleDateString('it-IT', options);
const capitalizedDate = dateStr.charAt(0).toUpperCase() + dateStr.slice(1);
const statusDateDisplay = document.getElementById('statusDateDisplay');
if (statusDateDisplay) statusDateDisplay.textContent = capitalizedDate;
const pickerDateDisplay = document.getElementById('pickerDateDisplay');
if (pickerDateDisplay) pickerDateDisplay.textContent = utils.formatDate(statusDate);
const summaryDateDisplay = document.getElementById('summaryDateDisplay');
if (summaryDateDisplay) summaryDateDisplay.textContent = dateStr;
const picker = document.getElementById('statusDatePicker');
if (picker) {
const yyyy = statusDate.getFullYear();
const mm = String(statusDate.getMonth() + 1).padStart(2, '0');
const dd = String(statusDate.getDate()).padStart(2, '0');
picker.value = `${yyyy}-${mm}-${dd}`;
}
}
async function loadDailyStatus() {
if (!currentUser || !currentUser.office_id) return;
const dateStr = utils.formatDate(statusDate);
const officeId = currentUser.office_id;
const grid = document.getElementById('spotsGrid');
// Keep grid height to avoid jump if possible, or just loading styling
if (grid) grid.innerHTML = 'Caricamento...
';
try {
const response = await api.get(`/api/parking/assignments/${dateStr}?office_id=${officeId}`);
if (response && response.ok) {
const assignments = await response.json();
renderParkingStatus(assignments);
} else {
if (grid) grid.innerHTML = 'Impossibile caricare i dati.
';
}
} catch (e) {
console.error("Error loading parking status", e);
if (grid) grid.innerHTML = 'Errore di caricamento.
';
}
}
function renderParkingStatus(assignments) {
const grid = document.getElementById('spotsGrid');
if (!grid) return;
grid.innerHTML = '';
if (!assignments || assignments.length === 0) {
grid.innerHTML = 'Nessun posto configurato o disponibile.
';
const badge = document.getElementById('spotsCountBadge');
if (badge) badge.textContent = `Liberi: 0/0`;
return;
}
// Sort
assignments.sort((a, b) => {
const nameA = a.spot_display_name || a.spot_id;
const nameB = b.spot_display_name || b.spot_id;
return nameA.localeCompare(nameB, undefined, { numeric: true, sensitivity: 'base' });
});
let total = assignments.length;
let free = 0;
assignments.forEach(a => {
const isFree = !a.user_id;
if (isFree) free++;
const spotName = a.spot_display_name || a.spot_id;
const statusText = isFree ? 'Libero' : (a.user_name || 'Occupato');
// Colors: Free = Green (default), Occupied = Yellow (requested)
// Yellow palette: Border #eab308, bg #fefce8, text #a16207, icon #eab308
const borderColor = isFree ? '#22c55e' : '#eab308';
const bgColor = isFree ? '#f0fdf4' : '#fefce8';
const textColor = isFree ? '#15803d' : '#a16207';
const iconColor = isFree ? '#22c55e' : '#eab308';
const el = document.createElement('div');
el.className = 'spot-card';
el.style.cssText = `
border: 1px solid ${borderColor};
background: ${bgColor};
border-radius: 8px;
padding: 1rem;
width: 140px;
min-width: 120px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
`;
// New Car Icon (Front Facing Sedan style or similar simple shape)
// Using a cleaner SVG path
el.innerHTML = `
${spotName}
${statusText}
`;
grid.appendChild(el);
});
const badge = document.getElementById('spotsCountBadge');
if (badge) badge.textContent = `Liberi: ${free}/${total}`;
}
function setupStatusListeners() {
const prevDay = document.getElementById('statusPrevDay');
if (prevDay) prevDay.addEventListener('click', () => {
statusDate.setDate(statusDate.getDate() - 1);
updateStatusHeader();
loadDailyStatus();
});
const nextDay = document.getElementById('statusNextDay');
if (nextDay) nextDay.addEventListener('click', () => {
statusDate.setDate(statusDate.getDate() + 1);
updateStatusHeader();
loadDailyStatus();
});
const datePicker = document.getElementById('statusDatePicker');
if (datePicker) datePicker.addEventListener('change', (e) => {
if (e.target.value) {
statusDate = new Date(e.target.value);
updateStatusHeader();
loadDailyStatus();
}
});
}