feat: aggiunti: loggica random, tema scuro, correzioni mail, miglioramenti generali, cache;
This commit is contained in:
@@ -59,7 +59,7 @@ function renderOffices() {
|
||||
const tbody = document.getElementById('officesBody');
|
||||
|
||||
if (offices.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Nessun ufficio trovato</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Nessun gruppo trovato</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function renderOffices() {
|
||||
<td>${office.user_count || 0} utenti</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick="editOffice('${office.id}')">Modifica</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteOffice('${office.id}')" ${office.user_count > 0 ? 'title="Impossibile eliminare uffici con utenti" disabled' : ''}>Elimina</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteOffice('${office.id}')" ${office.user_count > 0 ? 'title="Impossibile eliminare gruppi con utenti" disabled' : ''}>Elimina</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -103,30 +103,30 @@ async function editOffice(officeId) {
|
||||
document.getElementById('officeCutoffHour').value = office.booking_window_end_hour != null ? office.booking_window_end_hour : 18;
|
||||
document.getElementById('officeCutoffMinute').value = office.booking_window_end_minute != null ? office.booking_window_end_minute : 0;
|
||||
|
||||
openModal('Modifica Ufficio');
|
||||
openModal('Modifica Gruppo');
|
||||
}
|
||||
|
||||
async function deleteOffice(officeId) {
|
||||
const office = offices.find(o => o.id === officeId);
|
||||
if (!office) return;
|
||||
|
||||
if (!confirm(`Eliminare l'ufficio "${office.name}"?`)) return;
|
||||
if (!confirm(`Eliminare il gruppo "${office.name}"?`)) return;
|
||||
|
||||
const response = await api.delete(`/api/offices/${officeId}`);
|
||||
if (response && response.ok) {
|
||||
utils.showMessage('Ufficio eliminato', 'success');
|
||||
utils.showMessage('Gruppo eliminato', 'success');
|
||||
api.invalidateCache('/api/offices'); // Clear cache
|
||||
await loadOffices();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Impossibile eliminare l\'ufficio', 'error');
|
||||
utils.showMessage(error.detail || 'Impossibile eliminare il gruppo', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Add button
|
||||
document.getElementById('addOfficeBtn').addEventListener('click', () => {
|
||||
openModal('Nuovo Ufficio');
|
||||
openModal('Nuovo Gruppo');
|
||||
});
|
||||
|
||||
// Modal close
|
||||
@@ -177,7 +177,7 @@ async function handleOfficeSubmit(e) {
|
||||
|
||||
if (response && response.ok) {
|
||||
closeModal();
|
||||
utils.showMessage(officeId ? 'Ufficio aggiornato' : 'Ufficio creato', 'success');
|
||||
utils.showMessage(officeId ? 'Gruppo aggiornato' : 'Gruppo creato', 'success');
|
||||
api.invalidateCache('/api/offices'); // Clear cache
|
||||
await loadOffices();
|
||||
} else {
|
||||
|
||||
@@ -129,7 +129,7 @@ async function editUser(userId) {
|
||||
|
||||
// Populate office dropdown
|
||||
const officeSelect = document.getElementById('editOffice');
|
||||
officeSelect.innerHTML = '<option value="">Nessun ufficio</option>';
|
||||
officeSelect.innerHTML = '<option value="">Nessun gruppo</option>';
|
||||
offices.forEach(o => {
|
||||
const option = document.createElement('option');
|
||||
option.value = o.id;
|
||||
|
||||
@@ -42,6 +42,20 @@ const ICONS = {
|
||||
settings: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>`,
|
||||
sun: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>`,
|
||||
moon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
@@ -50,8 +64,8 @@ const NAV_ITEMS = [
|
||||
{ href: '/team-calendar', icon: 'users', label: 'Calendario del team' },
|
||||
{ href: '/team-rules', icon: 'rules', label: 'Regole parcheggio', roles: ['admin', 'manager'] },
|
||||
{ href: '/admin/users', icon: 'user', label: 'Gestione Utenti', roles: ['admin'] },
|
||||
{ href: '/admin/offices', icon: 'building', label: 'Gestione Uffici', roles: ['admin'] },
|
||||
{ href: '/parking-settings', icon: 'settings', label: 'Impostazioni Ufficio', roles: ['admin', 'manager'] }
|
||||
{ href: '/admin/offices', icon: 'building', label: 'Gestione Gruppi', roles: ['admin'] },
|
||||
{ href: '/parking-settings', icon: 'settings', label: 'Impostazioni Gruppi', roles: ['admin', 'manager'] }
|
||||
];
|
||||
|
||||
function getIcon(name) {
|
||||
@@ -98,9 +112,10 @@ async function initNav() {
|
||||
// Setup user menu (logout) & mobile menu
|
||||
setupUserMenu();
|
||||
setupMobileMenu();
|
||||
setupThemeToggle();
|
||||
|
||||
// CHECK: Block access if user has no office (and is not admin)
|
||||
// Admins are allowed to access "Gestione Uffici" even without an office
|
||||
// Admins are allowed to access "Gestione Gruppi" even without an office
|
||||
if (currentUser && !currentUser.office_id && currentUser.role !== 'admin') {
|
||||
navContainer.innerHTML = ''; // Clear nav
|
||||
|
||||
@@ -124,14 +139,14 @@ async function initNav() {
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style="margin-bottom: 1rem;">Ufficio non assegnato</h2>
|
||||
<h2 style="margin-bottom: 1rem;">Gruppo non assegnato</h2>
|
||||
<p class="text-secondary" style="margin-bottom: 1.5rem; line-height: 1.6;">
|
||||
Il tuo account <strong>${currentUser.email}</strong> è attivo, ma non sei ancora stato assegnato a nessuno ufficio.
|
||||
Il tuo account <strong>${currentUser.email}</strong> è attivo, ma non sei ancora stato assegnato a nessuno gruppo.
|
||||
</p>
|
||||
<div style="padding: 1.5rem; background: #f8fafc; border-radius: 8px; text-align: left;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text);">Cosa fare?</div>
|
||||
<div style="font-size: 0.95rem; color: var(--text-secondary);">
|
||||
Contatta l'amministratore di sistema per richiedere l'assegnazione al tuo ufficio di competenza.<br>
|
||||
Contatta l'amministratore di sistema per richiedere l'assegnazione al tuo gruppo di competenza.<br>
|
||||
<a href="mailto:s.salemi@sielte.it" style="color: var(--primary); text-decoration: none; font-weight: 500; margin-top: 0.5rem; display: inline-block;">s.salemi@sielte.it</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,6 +227,35 @@ function setupUserMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
function setupThemeToggle() {
|
||||
// Apply immediate theme to avoid flash
|
||||
const savedTheme = localStorage.getItem('theme') || 'system';
|
||||
applyTheme(savedTheme);
|
||||
|
||||
// Watch for system theme changes if set to system
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (localStorage.getItem('theme') === 'system') {
|
||||
applyTheme('system');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
let isDark = false;
|
||||
if (theme === 'system') {
|
||||
isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
} else {
|
||||
isDark = theme === 'dark';
|
||||
}
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.getIcon = getIcon;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ async function loadOffices() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
utils.showMessage('Errore caricamento uffici', 'error');
|
||||
utils.showMessage('Errore caricamento gruppi', 'error');
|
||||
}
|
||||
} else {
|
||||
// Manager uses their own office
|
||||
@@ -58,7 +58,7 @@ async function loadOffices() {
|
||||
if (currentUser.office_id) {
|
||||
await loadOfficeSettings(currentUser.office_id);
|
||||
} else {
|
||||
utils.showMessage('Nessun ufficio assegnato al manager', 'error');
|
||||
utils.showMessage('Nessun gruppo assegnato al manager', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ function populateHourSelect() {
|
||||
async function loadOfficeSettings(id) {
|
||||
const officeId = id;
|
||||
if (!officeId) {
|
||||
utils.showMessage('Nessun ufficio selezionato', 'error');
|
||||
utils.showMessage('Nessun gruppo selezionato', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ async function loadOfficeSettings(id) {
|
||||
currentOffice = office;
|
||||
|
||||
// Populate form
|
||||
document.getElementById('assignmentModeSelect').value = office.assignment_mode || 'fairness';
|
||||
document.getElementById('bookingWindowEnabled').checked = office.booking_window_enabled || false;
|
||||
document.getElementById('bookingWindowHour').value = office.booking_window_end_hour ?? 18; // Default 18
|
||||
document.getElementById('bookingWindowMinute').value = office.booking_window_end_minute ?? 0;
|
||||
@@ -136,6 +137,7 @@ function setupEventListeners() {
|
||||
if (!currentOffice) return;
|
||||
|
||||
const data = {
|
||||
assignment_mode: document.getElementById('assignmentModeSelect').value,
|
||||
booking_window_enabled: document.getElementById('bookingWindowEnabled').checked,
|
||||
booking_window_end_hour: parseInt(document.getElementById('bookingWindowHour').value),
|
||||
booking_window_end_minute: parseInt(document.getElementById('bookingWindowMinute').value)
|
||||
@@ -242,7 +244,7 @@ function setupEventListeners() {
|
||||
const clearPresenceBtn = document.getElementById('clearPresenceBtn');
|
||||
if (clearPresenceBtn) {
|
||||
clearPresenceBtn.addEventListener('click', async () => {
|
||||
if (!confirm('ATTENZIONE: Stai per eliminare TUTTI GLI STATI (Presente/Assente/ecc) e relative assegnazioni per tutti gli utenti dell\'ufficio nel periodo selezionato. \n\nQuesta azione è irreversibile. Procedere?')) return;
|
||||
if (!confirm('ATTENZIONE: Stai per eliminare TUTTI GLI STATI (Presente/Assente/ecc) e relative assegnazioni per tutti gli utenti del gruppo nel periodo selezionato. \n\nQuesta azione è irreversibile. Procedere?')) return;
|
||||
|
||||
const dateStart = document.getElementById('testDateStart').value;
|
||||
const dateEnd = document.getElementById('testDateEnd').value;
|
||||
@@ -251,7 +253,7 @@ function setupEventListeners() {
|
||||
|
||||
// Validate office
|
||||
if (!currentOffice || !currentOffice.id) {
|
||||
return utils.showMessage('Errore: Nessun ufficio selezionato', 'error');
|
||||
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
||||
}
|
||||
|
||||
const endDateVal = dateEnd || dateStart;
|
||||
@@ -286,7 +288,7 @@ function setupEventListeners() {
|
||||
|
||||
// Validate office
|
||||
if (!currentOffice || !currentOffice.id) {
|
||||
return utils.showMessage('Errore: Nessun ufficio selezionato', 'error');
|
||||
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
||||
}
|
||||
|
||||
utils.showMessage('Invio mail di test in corso...', 'warning');
|
||||
@@ -329,7 +331,7 @@ function setupEventListeners() {
|
||||
|
||||
// Validate office
|
||||
if (!currentOffice || !currentOffice.id) {
|
||||
return utils.showMessage('Errore: Nessun ufficio selezionato', 'error');
|
||||
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
||||
}
|
||||
|
||||
if (!dateVal) {
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function updateOfficeDisplay() {
|
||||
|
||||
// If user is employee, show their office name directly
|
||||
if (currentUser.role === 'employee') {
|
||||
display.textContent = currentUser.office_name || "Mio Ufficio";
|
||||
display.textContent = currentUser.office_name || "Mio Gruppo";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,10 +63,10 @@ function updateOfficeDisplay() {
|
||||
// let text = option.textContent.split('(')[0].trim();
|
||||
display.textContent = option.textContent;
|
||||
} else {
|
||||
display.textContent = "Tutti gli Uffici";
|
||||
display.textContent = "Tutti i Gruppi";
|
||||
}
|
||||
} else {
|
||||
display.textContent = "Tutti gli Uffici";
|
||||
display.textContent = "Tutti i Gruppi";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ function renderCalendar() {
|
||||
|
||||
// Build header row
|
||||
const dayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
|
||||
let headerHtml = '<th>Nome</th><th>Ufficio</th>';
|
||||
let headerHtml = '<th>Nome</th><th>Gruppo</th>';
|
||||
|
||||
for (let i = 0; i < dayCount; i++) {
|
||||
const date = new Date(startDate);
|
||||
@@ -348,8 +348,20 @@ function renderCalendar() {
|
||||
|
||||
let bodyHtml = '';
|
||||
teamData.forEach(member => {
|
||||
let nameHtml = member.name || 'Unknown';
|
||||
if (member.ratio !== undefined && member.ratio !== null) {
|
||||
nameHtml += `
|
||||
<span style="margin-left: 6px; font-size: 0.8rem; color: var(--text-secondary); cursor: help;" title="(Giorni in cui hai avuto parcheggio) / (Giorni in sede)">
|
||||
<svg style="vertical-align: text-bottom; margin-right: 2px;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
${member.ratio.toFixed(2)}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
bodyHtml += `<tr>
|
||||
<td class="member-name">${member.name || 'Unknown'}</td>
|
||||
<td class="member-name" style="text-align: left; padding-left: 1rem;">${nameHtml}</td>
|
||||
<td class="member-manager">${member.office_name || '-'}</td>`;
|
||||
|
||||
for (let i = 0; i < dayCount; i++) {
|
||||
@@ -384,7 +396,7 @@ function renderCalendar() {
|
||||
// (Already have dateStr)
|
||||
|
||||
const memberRules = officeClosingRules[member.office_id];
|
||||
let isClosed = false;
|
||||
let isClosed = isHoliday;
|
||||
|
||||
if (memberRules) {
|
||||
// Check weekly
|
||||
|
||||
@@ -134,6 +134,7 @@ async function saveWeeklyClosingDays() {
|
||||
|
||||
await Promise.all(promises);
|
||||
utils.showMessage('Giorni di chiusura aggiornati', 'success');
|
||||
api.invalidateCache(`/api/offices/${currentOfficeId}/weekly-closing-days`);
|
||||
await loadWeeklyClosingDays(currentOfficeId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -176,6 +177,7 @@ async function loadClosingDays(officeId) {
|
||||
async function addClosingDay(data) {
|
||||
const response = await api.post(`/api/offices/${currentOfficeId}/closing-days`, data);
|
||||
if (response && response.ok) {
|
||||
api.invalidateCache(`/api/offices/${currentOfficeId}/closing-days`);
|
||||
await loadClosingDays(currentOfficeId);
|
||||
document.getElementById('closingDayModal').style.display = 'none';
|
||||
document.getElementById('closingDayForm').reset();
|
||||
@@ -189,6 +191,7 @@ async function deleteClosingDay(id) {
|
||||
if (!confirm('Eliminare questo giorno di chiusura?')) return;
|
||||
const response = await api.delete(`/api/offices/${currentOfficeId}/closing-days/${id}`);
|
||||
if (response && response.ok) {
|
||||
api.invalidateCache(`/api/offices/${currentOfficeId}/closing-days`);
|
||||
await loadClosingDays(currentOfficeId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user