feat: aggiunti: loggica random, tema scuro, correzioni mail, miglioramenti generali, cache;

This commit is contained in:
StefanoSalemi
2026-04-17 18:27:37 +02:00
parent a7ef46640d
commit 104ad53a9a
26 changed files with 861 additions and 216 deletions

View File

@@ -175,11 +175,12 @@ function renderCalendar() {
return date >= start && date <= end;
});
const isClosed = isWeeklyClosed || isSpecificClosed;
const isClosed = isHoliday || isWeeklyClosed || isSpecificClosed;
if (isClosed) {
cell.classList.add('closed');
cell.title = "Ufficio Chiuso";
if (isHoliday) cell.title = "Festività";
else cell.title = "Gruppo Chiuso";
} else if (presence) {
cell.classList.add(`status-${presence.status}`);
}
@@ -418,10 +419,10 @@ function initParkingStatus() {
if (headerDisplay) headerDisplay.textContent = currentUser.office_name;
} else {
const nameDisplay = document.getElementById('statusOfficeName');
if (nameDisplay) nameDisplay.textContent = 'Tuo Ufficio';
if (nameDisplay) nameDisplay.textContent = 'Tuo Gruppo';
const headerDisplay = document.getElementById('currentOfficeDisplay');
if (headerDisplay) headerDisplay.textContent = 'Tuo Ufficio';
if (headerDisplay) headerDisplay.textContent = 'Tuo Gruppo';
}
}
@@ -455,6 +456,34 @@ async function loadDailyStatus() {
const officeId = currentUser.office_id;
const grid = document.getElementById('spotsGrid');
const badge = document.getElementById('spotsCountBadge');
// Check if it's a closing day
const dayOfWeek = statusDate.getDay();
const isHoliday = utils.isItalianHoliday(statusDate);
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;
start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0);
const check = new Date(statusDate); check.setHours(0, 0, 0, 0);
return check >= start && check <= end;
});
if (isHoliday || isWeeklyClosed || isSpecificClosed) {
if (grid) {
grid.innerHTML = `
<div style="width:100%; text-align:center; padding:1rem 1rem; color: var(--text-secondary); background: var(--bg-hover); border-radius: 8px;">
<div style="font-size: 1.2rem; font-weight: 500;">Ufficio Chiuso</div>
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.8;">Nessun parcheggio disponibile in questa data.</div>
</div>`;
}
if (badge) badge.style.display = 'none';
return;
} else {
if (badge) badge.style.display = 'inline-block';
}
// Keep grid height to avoid jump if possible, or just loading styling
if (grid) grid.innerHTML = '<div style="width:100%; text-align:center; padding:2rem; color:var(--text-secondary);">Caricamento...</div>';
@@ -505,10 +534,9 @@ function renderParkingStatus(assignments) {
// 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 borderColor = isFree ? 'var(--spot-free-border)' : 'var(--spot-occ-border)';
const bgColor = isFree ? 'var(--spot-free-bg)' : 'var(--spot-occ-bg)';
const textColor = isFree ? 'var(--spot-free-text)' : 'var(--spot-occ-text)';
const el = document.createElement('div');
el.className = 'spot-card';
@@ -527,15 +555,49 @@ function renderParkingStatus(assignments) {
transition: all 0.2s;
`;
// New Car Icon (Front Facing Sedan style or similar simple shape)
// Using a cleaner SVG path
el.innerHTML = `
<div style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">${spotName}</div>
<div style="font-weight: 600; font-size: 1.1rem; color: var(--text);">${spotName}</div>
<div style="color: ${textColor}; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;" title="${statusText}">
${statusText}
</div>
`;
if (isFree && (currentUser.role === 'admin' || currentUser.role === 'manager')) {
el.style.cursor = 'pointer';
el.addEventListener('mouseenter', () => el.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)');
el.addEventListener('mouseleave', () => el.style.boxShadow = 'none');
el.title = "Clicca per assegnare manualmente";
el.addEventListener('click', () => {
openAdminAssignModal(a.spot_id, spotName);
});
}
if (!isFree && (currentUser.role === 'admin' || currentUser.role === 'manager')) {
el.style.cursor = 'pointer';
el.addEventListener('mouseenter', () => el.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)');
el.addEventListener('mouseleave', () => el.style.boxShadow = 'none');
el.title = "Clicca per liberare questo posto";
el.addEventListener('click', async () => {
if (!confirm(`Vuoi liberare il posto ${spotName} occupato da ${statusText}?`)) return;
utils.showMessage('Rilascio in corso...', 'warning');
const response = await api.post('/api/parking/reassign-spot', {
assignment_id: a.id,
new_user_id: null
});
if (response && response.ok) {
utils.showMessage('Posto liberato con successo', 'success');
loadDailyStatus();
loadParkingAssignments();
renderCalendar();
} else {
const err = await response.json();
utils.showMessage(err.detail || 'Impossibile liberare il posto', 'error');
}
});
}
grid.appendChild(el);
});
@@ -569,6 +631,97 @@ function setupStatusListeners() {
loadDailyStatus();
}
});
setupAdminAssignModal();
}
// ----------------------------------------------------------------------------
// Admin Manual Assign Logic
// ----------------------------------------------------------------------------
let currentManualSpotId = null;
function setupAdminAssignModal() {
const closeBtn = document.getElementById('closeAdminAssignModal');
const cancelBtn = document.getElementById('cancelAdminAssign');
const form = document.getElementById('adminAssignForm');
const modal = document.getElementById('adminAssignSpotModal');
if (closeBtn) closeBtn.addEventListener('click', () => modal.style.display = 'none');
if (cancelBtn) cancelBtn.addEventListener('click', () => modal.style.display = 'none');
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const targetUserId = document.getElementById('adminAssignUser').value;
if (!targetUserId || !currentManualSpotId) return;
const dateStr = utils.formatDate(statusDate);
utils.showMessage('Assegnazione in corso...', 'warning');
const response = await api.post('/api/parking/manual-assign', {
date: dateStr,
user_id: targetUserId,
spot_id: currentManualSpotId,
office_id: currentUser.office_id
});
if (response && response.ok) {
utils.showMessage('Posto assegnato con successo', 'success');
modal.style.display = 'none';
loadDailyStatus(); // refresh parking status grid
// optionally refresh my presences if it affected the logged in admin
loadParkingAssignments();
} else {
const err = await response.json();
utils.showMessage(err.detail || 'Impossibile assegnare il parcheggio', 'error');
}
});
}
}
async function openAdminAssignModal(spotId, spotName) {
currentManualSpotId = spotId;
const modal = document.getElementById('adminAssignSpotModal');
const infoDisplay = document.getElementById('adminAssignSpotInfo');
const selectUser = document.getElementById('adminAssignUser');
const dateStr = utils.formatDate(statusDate);
infoDisplay.innerHTML = `Seleziona l'utente a cui assegnare il posto <strong>${spotName}</strong> per la giornata del <strong>${dateStr}</strong>.`;
selectUser.innerHTML = '<option value="">Caricamento...</option>';
modal.style.display = 'flex';
// Fetch team presences for the office effectively identifying people physically present but without parking
try {
const response = await api.get(`/api/presence/team?start_date=${dateStr}&end_date=${dateStr}&office_id=${currentUser.office_id}`);
if (response && response.ok) {
const result = await response.json();
const members = Array.isArray(result) ? result : [];
// Filter out people who already have parking
const availableMembers = members.filter(m => {
const presence = m.presences && m.presences.find(p => p.date === dateStr);
const hasParking = m.parking_dates && m.parking_dates.includes(dateStr);
return presence && presence.status === 'present' && !hasParking;
});
if (availableMembers.length === 0) {
selectUser.innerHTML = '<option value="">Nessun utente presente e senza parcheggio oggi...</option>';
} else {
selectUser.innerHTML = '<option value="">Seleziona utente...</option>';
availableMembers.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.name}`;
selectUser.appendChild(option);
});
}
}
} catch (e) {
console.error("Errore fetch team presences:", e);
console.error(e);
selectUser.innerHTML = '<option value="">Errore caricamento utenti</option>';
}
}