Primo commit

This commit is contained in:
2026-01-13 11:20:12 +01:00
parent ce9e2fdf2a
commit 17453f5d13
51 changed files with 3883 additions and 2508 deletions

View File

@@ -0,0 +1,167 @@
/**
* Admin Offices Page
* Manage offices, quotas, and prefixes
*/
let currentUser = null;
let offices = [];
document.addEventListener('DOMContentLoaded', async () => {
currentUser = await api.requireAuth();
if (!currentUser) return;
if (currentUser.role !== 'admin') {
window.location.href = '/presence';
return;
}
await loadOffices();
setupEventListeners();
});
async function loadOffices() {
const response = await api.get('/api/offices');
if (response && response.ok) {
offices = await response.json();
renderOffices();
}
}
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>';
return;
}
tbody.innerHTML = offices.map(office => {
return `
<tr>
<td>${office.name}</td>
<td><span class="badge badge-info">${office.parking_quota} posti</span></td>
<td><strong>${office.spot_prefix || '-'}</strong></td>
<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>
</td>
</tr>
`;
}).join('');
}
function openModal(title) {
document.getElementById('officeModalTitle').textContent = title;
document.getElementById('officeModal').style.display = 'flex';
}
function closeModal() {
document.getElementById('officeModal').style.display = 'none';
document.getElementById('officeForm').reset();
document.getElementById('officeId').value = '';
}
async function editOffice(officeId) {
const office = offices.find(o => o.id === officeId);
if (!office) return;
document.getElementById('officeId').value = office.id;
document.getElementById('officeName').value = office.name;
document.getElementById('officeQuota').value = office.parking_quota;
openModal('Modifica Ufficio');
}
async function deleteOffice(officeId) {
const office = offices.find(o => o.id === officeId);
if (!office) return;
if (!confirm(`Eliminare l'ufficio "${office.name}"?`)) return;
const response = await api.delete(`/api/offices/${officeId}`);
if (response && response.ok) {
utils.showMessage('Ufficio eliminato', 'success');
await loadOffices();
} else {
const error = await response.json();
utils.showMessage(error.detail || 'Impossibile eliminare l\'ufficio', 'error');
}
}
function setupEventListeners() {
// Add button
document.getElementById('addOfficeBtn').addEventListener('click', () => {
openModal('Nuovo Ufficio');
});
// Modal close
document.getElementById('closeOfficeModal').addEventListener('click', closeModal);
document.getElementById('cancelOffice').addEventListener('click', closeModal);
utils.setupModalClose('officeModal');
// Debug tracking for save button
const saveBtn = document.getElementById('saveOfficeBtn');
if (saveBtn) {
saveBtn.addEventListener('click', () => console.log('Save button clicked'));
}
// Form submit
const form = document.getElementById('officeForm');
form.addEventListener('submit', handleOfficeSubmit);
}
async function handleOfficeSubmit(e) {
e.preventDefault();
console.log('Form submitting...');
const saveBtn = document.getElementById('saveOfficeBtn');
const originalText = saveBtn.innerHTML;
saveBtn.disabled = true;
saveBtn.innerHTML = 'Salvataggio...';
const officeId = document.getElementById('officeId').value;
const data = {
name: document.getElementById('officeName').value,
parking_quota: parseInt(document.getElementById('officeQuota').value) || 0
};
console.log('Payload:', data);
try {
let response;
if (officeId) {
response = await api.put(`/api/offices/${officeId}`, data);
} else {
response = await api.post('/api/offices', data);
}
console.log('Response status:', response ? response.status : 'null');
if (response && response.ok) {
closeModal();
utils.showMessage(officeId ? 'Ufficio aggiornato' : 'Ufficio creato', 'success');
await loadOffices();
} else {
let errorMessage = 'Errore operazione';
try {
const error = await response.json();
errorMessage = error.detail || errorMessage;
} catch (e) {
console.error('Error parsing JSON error:', e);
errorMessage = 'Errore server imprevisto (' + (response ? response.status : 'network') + ')';
}
utils.showMessage(errorMessage, 'error');
}
} catch (error) {
console.error('Form submit exception:', error);
utils.showMessage('Errore di connessione: ' + error.message, 'error');
} finally {
saveBtn.disabled = false;
saveBtn.innerHTML = originalText;
}
}
// Global functions
window.editOffice = editOffice;
window.deleteOffice = deleteOffice;

View File

@@ -1,11 +1,12 @@
/**
* Admin Users Page
* Manage users with LDAP-aware editing
* Manage users with LDAP-aware editing and Office assignment
*/
let currentUser = null;
let users = [];
let managers = [];
let offices = [];
let currentSort = { column: 'name', direction: 'asc' };
document.addEventListener('DOMContentLoaded', async () => {
currentUser = await api.requireAuth();
@@ -16,15 +17,15 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
await loadManagers();
await loadOffices();
await loadUsers();
setupEventListeners();
});
async function loadManagers() {
const response = await api.get('/api/managers');
async function loadOffices() {
const response = await api.get('/api/offices');
if (response && response.ok) {
managers = await response.json();
offices = await response.json();
}
}
@@ -46,30 +47,60 @@ function renderUsers(filter = '') {
(u.name || '').toLowerCase().includes(filterLower) ||
(u.email || '').toLowerCase().includes(filterLower) ||
(u.role || '').toLowerCase().includes(filterLower) ||
(u.manager_name || '').toLowerCase().includes(filterLower)
(u.office_name || '').toLowerCase().includes(filterLower)
);
}
// Sort
filtered.sort((a, b) => {
let valA = a[currentSort.column];
let valB = b[currentSort.column];
// Handle nulls for ratio
if (currentSort.column === 'parking_ratio') {
valA = valA !== null ? valA : 999; // Null ratio (new users) -> low priority? No, new users have ratio 0.
// Actually get_user_parking_ratio returns 0.0 for new users.
// If office_id is missing, it's None. Treat as high val to push to bottom?
valA = (valA === undefined || valA === null) ? 999 : valA;
valB = (valB === undefined || valB === null) ? 999 : valB;
} else {
valA = (valA || '').toString().toLowerCase();
valB = (valB || '').toString().toLowerCase();
}
if (valA < valB) return currentSort.direction === 'asc' ? -1 : 1;
if (valA > valB) return currentSort.direction === 'asc' ? 1 : -1;
return 0;
});
// Update icons
document.querySelectorAll('th.sortable .sort-icon').forEach(icon => icon.textContent = '');
const activeTh = document.querySelector(`th[data-sort="${currentSort.column}"]`);
if (activeTh) {
const icon = activeTh.querySelector('.sort-icon');
if (icon) icon.textContent = currentSort.direction === 'asc' ? ' ▲' : ' ▼';
}
if (filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center">No users found</td></tr>';
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Nessun utente trovato</td></tr>';
return;
}
tbody.innerHTML = filtered.map(user => {
const ldapBadge = user.is_ldap_user ? '<span class="badge badge-info">LDAP</span>' : '';
const managerInfo = user.role === 'manager'
? `<span class="badge badge-success">${user.managed_user_count || 0} users</span>`
: (user.manager_name || '-');
const officeInfo = user.office_name || '-';
return `
<tr>
<td>${user.name || '-'} ${ldapBadge}</td>
<td>${user.email}</td>
<td><span class="badge badge-${getRoleBadgeClass(user.role)}">${user.role}</span></td>
<td>${managerInfo}</td>
<td>${officeInfo}</td>
<td>${user.parking_ratio !== null ? user.parking_ratio.toFixed(2) : '-'}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="editUser('${user.id}')">Edit</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')" ${user.id === currentUser.id ? 'disabled' : ''}>Delete</button>
<button class="btn btn-sm btn-secondary" onclick="editUser('${user.id}')">Modifica</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')" ${user.id === currentUser.id ? 'disabled' : ''}>Elimina</button>
</td>
</tr>
`;
@@ -93,20 +124,16 @@ async function editUser(userId) {
document.getElementById('editName').value = user.name || '';
document.getElementById('editEmail').value = user.email;
document.getElementById('editRole').value = user.role;
document.getElementById('editQuota').value = user.manager_parking_quota || 0;
document.getElementById('editPrefix').value = user.manager_spot_prefix || '';
// Populate manager dropdown
const managerSelect = document.getElementById('editManager');
managerSelect.innerHTML = '<option value="">No manager</option>';
managers.forEach(m => {
if (m.id !== userId) { // Can't be own manager
const option = document.createElement('option');
option.value = m.id;
option.textContent = m.name;
if (m.id === user.manager_id) option.selected = true;
managerSelect.appendChild(option);
}
// Populate office dropdown
const officeSelect = document.getElementById('editOffice');
officeSelect.innerHTML = '<option value="">Nessun ufficio</option>';
offices.forEach(o => {
const option = document.createElement('option');
option.value = o.id;
option.textContent = o.name;
if (o.id === user.office_id) option.selected = true;
officeSelect.appendChild(option);
});
// Handle LDAP restrictions
@@ -126,13 +153,7 @@ async function editUser(userId) {
roleSelect.disabled = isLdapAdmin;
document.getElementById('roleHelp').style.display = isLdapAdmin ? 'block' : 'none';
// Manager group - show for all users (admins can also be assigned to a manager)
document.getElementById('managerGroup').style.display = 'block';
// Manager fields - show only for managers
document.getElementById('managerFields').style.display = user.role === 'manager' ? 'block' : 'none';
document.getElementById('userModalTitle').textContent = 'Edit User';
document.getElementById('userModalTitle').textContent = 'Modifica Utente';
document.getElementById('userModal').style.display = 'flex';
}
@@ -140,15 +161,15 @@ async function deleteUser(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
if (!confirm(`Delete user "${user.name || user.email}"?`)) return;
if (!confirm(`Eliminare l'utente "${user.name || user.email}"?`)) return;
const response = await api.delete(`/api/users/${userId}`);
if (response && response.ok) {
utils.showMessage('User deleted', 'success');
utils.showMessage('Utente eliminato', 'success');
await loadUsers();
} else {
const error = await response.json();
utils.showMessage(error.detail || 'Failed to delete user', 'error');
utils.showMessage(error.detail || 'Impossibile eliminare l\'utente', 'error');
}
}
@@ -158,13 +179,6 @@ function setupEventListeners() {
renderUsers(e.target.value);
});
// Role change - toggle manager fields (manager group always visible since any user can have a manager)
document.getElementById('editRole').addEventListener('change', (e) => {
const role = e.target.value;
document.getElementById('managerFields').style.display = role === 'manager' ? 'block' : 'none';
// Manager group stays visible - any user (including admins) can have a manager assigned
});
// Modal close
document.getElementById('closeUserModal').addEventListener('click', () => {
document.getElementById('userModal').style.display = 'none';
@@ -183,7 +197,7 @@ function setupEventListeners() {
const data = {
role: role,
manager_id: document.getElementById('editManager').value || null
office_id: document.getElementById('editOffice').value || null
};
// Only include name if not disabled (LDAP users can't change name)
@@ -192,23 +206,31 @@ function setupEventListeners() {
data.name = nameInput.value;
}
// Manager-specific fields
if (role === 'manager') {
data.manager_parking_quota = parseInt(document.getElementById('editQuota').value) || 0;
data.manager_spot_prefix = document.getElementById('editPrefix').value || null;
}
const response = await api.put(`/api/users/${userId}`, data);
if (response && response.ok) {
document.getElementById('userModal').style.display = 'none';
utils.showMessage('User updated', 'success');
await loadManagers(); // Reload in case role changed
utils.showMessage('Utente aggiornato', 'success');
await loadUsers();
} else {
const error = await response.json();
utils.showMessage(error.detail || 'Failed to update user', 'error');
utils.showMessage(error.detail || 'Impossibile aggiornare l\'utente', 'error');
}
});
// Sort headers
document.querySelectorAll('th.sortable').forEach(th => {
th.addEventListener('click', () => {
const column = th.dataset.sort;
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'asc';
}
renderUsers(document.getElementById('searchInput').value);
});
});
}
// Make functions available globally for onclick handlers

View File

@@ -158,7 +158,7 @@ const api = {
}
const error = await response.json();
return { success: false, error: error.detail || 'Login failed' };
return { success: false, error: error.detail || 'Login fallito' };
},
/**
@@ -178,7 +178,7 @@ const api = {
}
const error = await response.json();
return { success: false, error: error.detail || 'Registration failed' };
return { success: false, error: error.detail || 'Registrazione fallita' };
},
/**

188
frontend/js/modal-logic.js Normal file
View File

@@ -0,0 +1,188 @@
/**
* Shared logic for the Day/Presence Modal
* Handles UI interactions, assignments, and integrated reassign form.
*/
const ModalLogic = {
// Configuration holding callbacks
config: {
onMarkPresence: async (status, date, userId) => { },
onClearPresence: async (date, userId) => { },
onReleaseParking: async (assignmentId) => { },
onReassignParking: async (assignmentId, newUserId) => { },
onReload: async () => { } // Callback to reload calendar data
},
// State
currentAssignmentId: null,
currentDate: null,
currentUserId: null,
init(config) {
this.config = { ...this.config, ...config };
this.setupEventListeners();
},
setupEventListeners() {
// Close buttons
document.getElementById('closeDayModal')?.addEventListener('click', () => {
document.getElementById('dayModal').style.display = 'none';
});
// Status buttons
document.querySelectorAll('#dayModal .status-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (this.currentDate) {
this.config.onMarkPresence(btn.dataset.status, this.currentDate, this.currentUserId);
}
});
});
// Actions
document.getElementById('clearDayBtn')?.addEventListener('click', () => {
if (this.currentDate) {
this.config.onClearPresence(this.currentDate, this.currentUserId);
}
});
document.getElementById('releaseParkingBtn')?.addEventListener('click', () => {
if (this.currentAssignmentId) {
this.config.onReleaseParking(this.currentAssignmentId);
}
});
document.getElementById('reassignParkingBtn')?.addEventListener('click', () => {
this.showReassignForm();
});
// Reassign Form
document.getElementById('cancelReassign')?.addEventListener('click', () => {
this.hideReassignForm();
});
document.getElementById('confirmReassign')?.addEventListener('click', () => {
const select = document.getElementById('reassignUser');
this.config.onReassignParking(this.currentAssignmentId, select.value);
});
// Close on click outside
window.addEventListener('click', (e) => {
const modal = document.getElementById('dayModal');
if (e.target === modal) {
modal.style.display = 'none';
}
});
},
openModal(data) {
const { dateStr, userName, presence, parking, userId, isReadOnly } = data;
this.currentDate = dateStr;
this.currentUserId = userId; // Optional, for team view
this.currentAssignmentId = parking ? parking.id : null;
const modal = document.getElementById('dayModal');
const title = document.getElementById('dayModalTitle');
const userLabel = document.getElementById('dayModalUser');
title.textContent = utils.formatDateDisplay(dateStr);
// Show/Hide User Name (for Team Calendar)
if (userName && userLabel) {
userLabel.textContent = userName;
userLabel.style.display = 'block';
} else if (userLabel) {
userLabel.style.display = 'none';
}
// Highlight status
document.querySelectorAll('#dayModal .status-btn').forEach(btn => {
const status = btn.dataset.status;
if (presence && presence.status === status) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// Clear button visibility
const clearBtn = document.getElementById('clearDayBtn');
if (presence) {
clearBtn.style.display = 'block';
} else {
clearBtn.style.display = 'none';
}
// Parking Section & Reset Form
this.hideReassignForm(); // Reset view to actions
const parkingSection = document.getElementById('parkingSection');
const parkingInfo = document.getElementById('parkingInfo');
if (parking) {
parkingSection.style.display = 'block';
const spotName = parking.spot_display_name || parking.spot_id;
parkingInfo.innerHTML = `<strong>Parcheggio:</strong> Posto ${spotName}`;
} else {
parkingSection.style.display = 'none';
}
modal.style.display = 'flex';
},
async showReassignForm() {
if (!this.currentAssignmentId) return;
const actionsDiv = document.getElementById('parkingActions');
const formDiv = document.getElementById('reassignForm');
actionsDiv.style.display = 'none';
formDiv.style.display = 'flex';
const select = document.getElementById('reassignUser');
select.innerHTML = '<option value="">Caricamento utenti...</option>';
// Load eligible users (Global API function assumed available)
try {
const response = await api.get(`/api/parking/eligible-users/${this.currentAssignmentId}`);
if (!response || !response.ok) {
const error = await response.json();
alert(error.detail || 'Impossibile caricare gli utenti idonei');
this.hideReassignForm();
return;
}
const users = await response.json();
select.innerHTML = '<option value="">Seleziona utente...</option>';
select.innerHTML += '<option value="auto">Assegna automaticamente</option>';
if (users.length === 0) {
const option = document.createElement('option');
option.disabled = true;
option.textContent = "Nessun altro utente disponibile";
select.appendChild(option);
} else {
users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
const officeInfo = user.office_name ? ` (${user.office_name})` : '';
option.textContent = user.name + officeInfo;
select.appendChild(option);
});
}
} catch (e) {
console.error(e);
alert('Errore di rete');
this.hideReassignForm();
}
},
hideReassignForm() {
document.getElementById('reassignForm').style.display = 'none';
document.getElementById('parkingActions').style.display = 'flex';
},
closeModal() {
document.getElementById('dayModal').style.display = 'none';
}
};

View File

@@ -38,14 +38,20 @@ const ICONS = {
<rect x="13.5" y="11" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="9" y="16" width="1.5" height="1.5" fill="currentColor"></rect>
<rect x="13.5" y="16" width="1.5" height="1.5" fill="currentColor"></rect>
</svg>`,
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>`
};
const NAV_ITEMS = [
{ href: '/presence', icon: 'calendar', label: 'My Presence' },
{ href: '/team-calendar', icon: 'users', label: 'Team Calendar' },
{ href: '/team-rules', icon: 'rules', label: 'Team Rules', roles: ['admin', 'manager'] },
{ href: '/admin/users', icon: 'user', label: 'Manage Users', roles: ['admin'] }
{ href: '/presence', icon: 'calendar', label: 'La mia presenza' },
{ 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'] }
];
function getIcon(name) {
@@ -108,7 +114,7 @@ function setupMobileMenu() {
const menuToggle = document.createElement('button');
menuToggle.className = 'menu-toggle';
menuToggle.innerHTML = MENU_ICON;
menuToggle.setAttribute('aria-label', 'Toggle menu');
menuToggle.setAttribute('aria-label', 'Apri/Chiudi menu');
pageHeader.insertBefore(menuToggle, pageHeader.firstChild);
// Add overlay

View File

@@ -0,0 +1,226 @@
let currentUser = null;
let currentOffice = null;
document.addEventListener('DOMContentLoaded', async () => {
if (!api.requireAuth()) return;
currentUser = await api.getCurrentUser();
if (!currentUser) return;
// Only Manager or Admin
if (!['admin', 'manager'].includes(currentUser.role)) {
window.location.href = '/';
return;
}
// Initialize UI
populateHourSelect();
// Set default date to tomorrow
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
document.getElementById('testDateStart').valueAsDate = tomorrow;
await loadOffices();
setupEventListeners();
});
async function loadOffices() {
const select = document.getElementById('officeSelect');
const card = document.getElementById('officeSelectionCard');
const content = document.getElementById('settingsContent');
// Only Admins see the selector
if (currentUser.role === 'admin') {
card.style.display = 'block';
content.style.display = 'none'; // Hide until selected
try {
const response = await api.get('/api/offices');
if (response && response.ok) {
const offices = await response.json();
offices.forEach(office => {
const option = document.createElement('option');
option.value = office.id;
option.textContent = office.name;
select.appendChild(option);
});
}
} catch (e) {
console.error(e);
utils.showMessage('Errore caricamento uffici', 'error');
}
} else {
// Manager uses their own office
card.style.display = 'none';
content.style.display = 'block';
if (currentUser.office_id) {
await loadOfficeSettings(currentUser.office_id);
} else {
utils.showMessage('Nessun ufficio assegnato al manager', 'error');
}
}
}
function populateHourSelect() {
const select = document.getElementById('bookingWindowHour');
select.innerHTML = '';
for (let h = 0; h < 24; h++) {
const option = document.createElement('option');
option.value = h;
option.textContent = h.toString().padStart(2, '0');
select.appendChild(option);
}
}
async function loadOfficeSettings(id) {
const officeId = id;
if (!officeId) {
utils.showMessage('Nessun ufficio selezionato', 'error');
return;
}
try {
const response = await api.get(`/api/offices/${officeId}`);
if (!response.ok) throw new Error('Failed to load office');
const office = await response.json();
currentOffice = office;
// Populate form
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;
updateVisibility();
} catch (e) {
console.error(e);
utils.showMessage('Errore nel caricamento impostazioni', 'error');
}
}
function updateVisibility() {
const enabled = document.getElementById('bookingWindowEnabled').checked;
document.getElementById('cutoffTimeGroup').style.display = enabled ? 'block' : 'none';
}
function setupEventListeners() {
// Office Select
document.getElementById('officeSelect').addEventListener('change', (e) => {
const id = e.target.value;
if (id) {
document.getElementById('settingsContent').style.display = 'block';
loadOfficeSettings(id);
} else {
document.getElementById('settingsContent').style.display = 'none';
}
});
// Toggle visibility
document.getElementById('bookingWindowEnabled').addEventListener('change', updateVisibility);
// Save Settings
document.getElementById('scheduleForm').addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentOffice) return;
const data = {
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)
};
try {
const res = await api.put(`/api/offices/${currentOffice.id}`, data);
if (res) {
utils.showMessage('Impostazioni salvate con successo', 'success');
currentOffice = res;
}
} catch (e) {
utils.showMessage('Errore nel salvataggio', 'error');
}
});
// Test Tools
document.getElementById('runAllocationBtn').addEventListener('click', async () => {
if (!confirm('Sei sicuro di voler avviare l\'assegnazione ORA? Questo potrebbe sovrascrivere le assegnazioni esistenti per la data selezionata.')) return;
const dateStart = document.getElementById('testDateStart').value;
const dateEnd = document.getElementById('testDateEnd').value;
if (!dateStart) return utils.showMessage('Seleziona una data di inizio', 'error');
let start = new Date(dateStart);
let end = dateEnd ? new Date(dateEnd) : new Date(dateStart);
if (end < start) {
return utils.showMessage('La data di fine deve essere successiva alla data di inizio', 'error');
}
let current = new Date(start);
let successCount = 0;
let errorCount = 0;
utils.showMessage('Avvio assegnazione...', 'success');
while (current <= end) {
const dateStr = current.toISOString().split('T')[0];
try {
await api.post('/api/parking/run-allocation', {
date: dateStr,
office_id: currentOffice.id
});
successCount++;
} catch (e) {
console.error(`Error for ${dateStr}`, e);
errorCount++;
}
current.setDate(current.getDate() + 1);
}
if (errorCount === 0) {
utils.showMessage(`Assegnazione completata per ${successCount} giorni.`, 'success');
} else {
utils.showMessage(`Completato con errori: ${successCount} successi, ${errorCount} errori.`, 'warning');
}
});
document.getElementById('clearAssignmentsBtn').addEventListener('click', async () => {
if (!confirm('ATTENZIONE: Stai per eliminare TUTTE le assegnazioni per il periodo selezionato. Procedere?')) return;
const dateStart = document.getElementById('testDateStart').value;
const dateEnd = document.getElementById('testDateEnd').value;
if (!dateStart) return utils.showMessage('Seleziona una data di inizio', 'error');
let start = new Date(dateStart);
let end = dateEnd ? new Date(dateEnd) : new Date(dateStart);
if (end < start) {
return utils.showMessage('La data di fine deve essere successiva alla data di inizio', 'error');
}
let current = new Date(start);
let totalRemoved = 0;
utils.showMessage('Rimozione in corso...', 'warning');
while (current <= end) {
const dateStr = current.toISOString().split('T')[0];
try {
const res = await api.post('/api/parking/clear-assignments', {
date: dateStr,
office_id: currentOffice.id
});
totalRemoved += (res.count || 0);
} catch (e) {
console.error(`Error clearing ${dateStr}`, e);
}
current.setDate(current.getDate() + 1);
}
utils.showMessage(`Operazione eseguita. ${totalRemoved} assegnazioni rimosse in totale.`, 'warning');
});
}

View File

@@ -8,14 +8,31 @@ 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()]);
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() {
@@ -56,10 +73,32 @@ async function loadParkingAssignments() {
}
}
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 || 0; // 0=Sunday, 1=Monday
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}`;
@@ -78,7 +117,7 @@ function renderCalendar() {
grid.innerHTML = '';
// Day headers - reorder based on week start day
const allDayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const allDayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
const dayNames = [];
for (let i = 0; i < 7; i++) {
dayNames.push(allDayNames[(weekStartDay + i) % 7]);
@@ -120,7 +159,25 @@ function renderCalendar() {
if (isHoliday) cell.classList.add('holiday');
if (isToday) cell.classList.add('today');
if (presence) {
// 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}`);
}
@@ -134,140 +191,60 @@ function renderCalendar() {
${parkingBadge}
`;
cell.addEventListener('click', () => openDayModal(dateStr, presence, parking));
if (!isClosed) {
cell.addEventListener('click', () => openDayModal(dateStr, presence, parking));
}
grid.appendChild(cell);
}
}
function openDayModal(dateStr, presence, parking) {
const modal = document.getElementById('dayModal');
const title = document.getElementById('dayModalTitle');
title.textContent = utils.formatDateDisplay(dateStr);
// Highlight current status
document.querySelectorAll('.status-btn').forEach(btn => {
const status = btn.dataset.status;
if (presence && presence.status === status) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
ModalLogic.openModal({
dateStr,
presence,
parking
});
// Update parking section
const parkingSection = document.getElementById('parkingSection');
const parkingInfo = document.getElementById('parkingInfo');
const releaseBtn = document.getElementById('releaseParkingBtn');
if (parking) {
parkingSection.style.display = 'block';
const spotName = parking.spot_display_name || parking.spot_id;
parkingInfo.innerHTML = `<strong>Parking:</strong> Spot ${spotName}`;
releaseBtn.dataset.assignmentId = parking.id;
document.getElementById('reassignParkingBtn').dataset.assignmentId = parking.id;
currentAssignmentId = parking.id;
} else {
parkingSection.style.display = 'none';
}
modal.dataset.date = dateStr;
modal.style.display = 'flex';
}
async function markPresence(status) {
const modal = document.getElementById('dayModal');
const date = modal.dataset.date;
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();
modal.style.display = 'none';
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Failed to mark presence');
alert(error.detail || 'Impossibile segnare la presenza');
}
}
async function clearPresence() {
const modal = document.getElementById('dayModal');
const date = modal.dataset.date;
if (!confirm('Clear presence for this date?')) return;
async function handleClearPresence(date) {
const response = await api.delete(`/api/presence/${date}`);
if (response && response.ok) {
await Promise.all([loadPresences(), loadParkingAssignments()]);
renderCalendar();
modal.style.display = 'none';
ModalLogic.closeModal();
}
}
async function releaseParking() {
const modal = document.getElementById('dayModal');
const releaseBtn = document.getElementById('releaseParkingBtn');
const assignmentId = releaseBtn.dataset.assignmentId;
if (!assignmentId) return;
if (!confirm('Release your parking spot for this date?')) return;
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();
modal.style.display = 'none';
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Failed to release parking spot');
alert(error.detail || 'Impossibile rilasciare il parcheggio');
}
}
async function openReassignModal() {
const assignmentId = currentAssignmentId;
if (!assignmentId) return;
// Load eligible users
const response = await api.get(`/api/parking/eligible-users/${assignmentId}`);
if (!response || !response.ok) {
const error = await response.json();
alert(error.detail || 'Failed to load eligible users');
return;
}
const users = await response.json();
const select = document.getElementById('reassignUser');
select.innerHTML = '<option value="">Select user...</option>';
if (users.length === 0) {
select.innerHTML = '<option value="">No eligible users available</option>';
} else {
users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.name;
select.appendChild(option);
});
}
// Get spot info from parking data
const parking = Object.values(parkingData).find(p => p.id === assignmentId);
if (parking) {
const spotName = parking.spot_display_name || parking.spot_id;
document.getElementById('reassignSpotInfo').textContent = `Spot ${spotName}`;
}
document.getElementById('dayModal').style.display = 'none';
document.getElementById('reassignModal').style.display = 'flex';
}
async function confirmReassign() {
const assignmentId = currentAssignmentId;
const newUserId = document.getElementById('reassignUser').value;
async function handleReassignParking(assignmentId, newUserId) {
// Basic validation handled by select; confirm
if (!assignmentId || !newUserId) {
alert('Please select a user');
alert('Seleziona un utente');
return;
}
@@ -279,13 +256,15 @@ async function confirmReassign() {
if (response && response.ok) {
await loadParkingAssignments();
renderCalendar();
document.getElementById('reassignModal').style.display = 'none';
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Failed to reassign parking spot');
alert(error.detail || 'Impossibile riassegnare il parcheggio');
}
}
function setupEventListeners() {
// Month navigation
document.getElementById('prevMonth').addEventListener('click', async () => {
@@ -300,69 +279,255 @@ function setupEventListeners() {
renderCalendar();
});
// Day modal
document.getElementById('closeDayModal').addEventListener('click', () => {
document.getElementById('dayModal').style.display = 'none';
// 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;
});
});
document.querySelectorAll('.status-btn').forEach(btn => {
btn.addEventListener('click', () => markPresence(btn.dataset.status));
});
if (quickEntryForm) {
quickEntryForm.addEventListener('submit', async (e) => {
e.preventDefault();
document.getElementById('clearDayBtn').addEventListener('click', clearPresence);
document.getElementById('releaseParkingBtn').addEventListener('click', releaseParking);
document.getElementById('reassignParkingBtn').addEventListener('click', openReassignModal);
const startStr = document.getElementById('qeStartDate').value;
const endStr = document.getElementById('qeEndDate').value;
const status = document.getElementById('qeStatus').value;
utils.setupModalClose('dayModal');
if (!status) return utils.showMessage('Seleziona uno stato', 'error');
if (!startStr || !endStr) return utils.showMessage('Seleziona le date', 'error');
// Reassign modal
document.getElementById('closeReassignModal').addEventListener('click', () => {
document.getElementById('reassignModal').style.display = 'none';
});
document.getElementById('cancelReassign').addEventListener('click', () => {
document.getElementById('reassignModal').style.display = 'none';
});
document.getElementById('confirmReassign').addEventListener('click', confirmReassign);
utils.setupModalClose('reassignModal');
const startDate = new Date(startStr);
const endDate = new Date(endStr);
// Bulk mark
document.getElementById('bulkMarkBtn').addEventListener('click', () => {
document.getElementById('bulkMarkModal').style.display = 'flex';
});
if (endDate < startDate) return utils.showMessage('La data di fine non può essere precedente alla data di inizio', 'error');
document.getElementById('closeBulkModal').addEventListener('click', () => {
document.getElementById('bulkMarkModal').style.display = 'none';
});
quickEntryModal.style.display = 'none';
utils.showMessage('Inserimento in corso...', 'warning');
document.getElementById('cancelBulk').addEventListener('click', () => {
document.getElementById('bulkMarkModal').style.display = 'none';
});
const promises = [];
let current = new Date(startDate);
utils.setupModalClose('bulkMarkModal');
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);
}
document.getElementById('bulkMarkForm').addEventListener('submit', async (e) => {
e.preventDefault();
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');
}
});
}
}
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const status = document.getElementById('bulkStatus').value;
const weekdaysOnly = document.getElementById('weekdaysOnly').checked;
// ----------------------------------------------------------------------------
// Parking Status Logic
// ----------------------------------------------------------------------------
const data = { start_date: startDate, end_date: endDate, status };
if (weekdaysOnly) {
data.days = [1, 2, 3, 4, 5]; // Mon-Fri (JS weekday)
}
function initParkingStatus() {
updateStatusHeader();
loadDailyStatus();
const response = await api.post('/api/presence/mark-bulk', data);
// 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 = '<div style="width:100%; text-align:center; padding:2rem; color:var(--text-secondary);">Caricamento...</div>';
try {
const response = await api.get(`/api/parking/assignments/${dateStr}?office_id=${officeId}`);
if (response && response.ok) {
const results = await response.json();
alert(`Marked ${results.length} dates`);
document.getElementById('bulkMarkModal').style.display = 'none';
await Promise.all([loadPresences(), loadParkingAssignments()]);
renderCalendar();
const assignments = await response.json();
renderParkingStatus(assignments);
} else {
const error = await response.json();
alert(error.detail || 'Failed to bulk mark');
if (grid) grid.innerHTML = '<div style="width:100%; text-align:center; padding:1rem;">Impossibile caricare i dati.</div>';
}
} catch (e) {
console.error("Error loading parking status", e);
if (grid) grid.innerHTML = '<div style="width:100%; text-align:center; padding:1rem;">Errore di caricamento.</div>';
}
}
function renderParkingStatus(assignments) {
const grid = document.getElementById('spotsGrid');
if (!grid) return;
grid.innerHTML = '';
if (!assignments || assignments.length === 0) {
grid.innerHTML = '<div style="width:100%; text-align:center; padding:1rem;">Nessun posto configurato o disponibile.</div>';
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 = `
<div style="font-weight: 600; font-size: 1.1rem; color: #1f2937;">${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>
`;
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();
}
});
}

View File

@@ -1,72 +1,118 @@
/**
* Team Calendar Page
* Shows presence and parking for all team members
* Filtered by manager (manager-centric model)
* Filtered by office (office-centric model)
*/
let currentUser = null;
let currentStartDate = null;
let viewMode = 'week'; // 'week' or 'month'
let managers = [];
let offices = [];
let teamData = [];
let parkingDataLookup = {};
let parkingAssignmentLookup = {};
let selectedUserId = null;
let selectedDate = null;
let currentAssignmentId = null;
let officeClosingRules = {}; // { officeId: { weekly: [], specific: [] } }
document.addEventListener('DOMContentLoaded', async () => {
currentUser = await api.requireAuth();
if (!currentUser) return;
// Initialize start date based on week start preference
const weekStartDay = currentUser.week_start_day || 0;
const weekStartDay = currentUser.week_start_day || 1;
currentStartDate = utils.getWeekStart(new Date(), weekStartDay);
await loadManagers();
await loadOffices();
await loadTeamData();
await loadTeamData();
// Initialize Modal Logic
ModalLogic.init({
onMarkPresence: handleMarkPresence,
onClearPresence: handleClearPresence,
onReleaseParking: handleReleaseParking,
onReassignParking: handleReassignParking
});
renderCalendar();
setupEventListeners();
});
async function loadManagers() {
const response = await api.get('/api/managers');
if (response && response.ok) {
managers = await response.json();
const select = document.getElementById('managerFilter');
function updateOfficeDisplay() {
const display = document.getElementById('currentOfficeNameDisplay');
if (!display) return;
// Filter managers based on user role
let filteredManagers = managers;
const select = document.getElementById('officeFilter');
// If user is employee, show their office name directly
if (currentUser.role === 'employee') {
display.textContent = currentUser.office_name || "Mio Ufficio";
return;
}
// For admin/manager, show selected
if (select && select.value) {
// Find name in options
const option = select.options[select.selectedIndex];
if (option) {
// Remove the count (xx utenti) part if desired, or keep it.
// User requested "nome del'ufficio", let's keep it simple.
// Option text is "Name (Count users)"
// let text = option.textContent.split('(')[0].trim();
display.textContent = option.textContent;
} else {
display.textContent = "Tutti gli Uffici";
}
} else {
display.textContent = "Tutti gli Uffici";
}
}
async function loadOffices() {
const select = document.getElementById('officeFilter');
// Only Admins and Managers can list offices
// Employees will just see their own office logic handled in loadTeamData
// Only Admins can see the office selector
if (currentUser.role !== 'admin') {
select.style.display = 'none';
// Employees stop here, Managers continue to allow auto-selection logic below
if (currentUser.role === 'employee') return;
}
const response = await api.get('/api/offices');
if (response && response.ok) {
offices = await response.json();
let filteredOffices = offices;
if (currentUser.role === 'manager') {
// Manager only sees themselves
filteredManagers = managers.filter(m => m.id === currentUser.id);
} else if (currentUser.role === 'employee') {
// Employee only sees their own manager
if (currentUser.manager_id) {
filteredManagers = managers.filter(m => m.id === currentUser.manager_id);
// Manager only sees their own office in the filter?
// Actually managers might want to filter if they (hypothetically) managed multiple,
// but currently User has 1 office.
if (currentUser.office_id) {
filteredOffices = offices.filter(o => o.id === currentUser.office_id);
} else {
filteredManagers = [];
filteredOffices = [];
}
}
filteredManagers.forEach(manager => {
filteredOffices.forEach(office => {
const option = document.createElement('option');
option.value = manager.id;
const userCount = manager.managed_user_count || 0;
option.textContent = `${manager.name} (${userCount} users)`;
option.value = office.id;
option.textContent = `${office.name} (${office.user_count || 0} utenti)`;
select.appendChild(option);
});
// Auto-select for managers and employees (they only see their team)
if (filteredManagers.length === 1) {
select.value = filteredManagers[0].id;
}
// Hide manager filter for employees (they can only see their team)
if (currentUser.role === 'employee') {
select.style.display = 'none';
// Auto-select for managers
if (currentUser.role === 'manager' && filteredOffices.length === 1) {
select.value = filteredOffices[0].id;
}
}
// Initial update of office display
updateOfficeDisplay();
}
function getDateRange() {
@@ -85,15 +131,16 @@ function getDateRange() {
}
async function loadTeamData() {
await loadClosingData();
const { startDate, endDate } = getDateRange();
const startStr = utils.formatDate(startDate);
const endStr = utils.formatDate(endDate);
let url = `/api/presence/team?start_date=${startStr}&end_date=${endStr}`;
const managerFilter = document.getElementById('managerFilter').value;
if (managerFilter) {
url += `&manager_id=${managerFilter}`;
const officeFilter = document.getElementById('officeFilter').value;
if (officeFilter) {
url += `&office_id=${officeFilter}`;
}
const response = await api.get(url);
@@ -114,6 +161,68 @@ async function loadTeamData() {
}
}
async function loadClosingData() {
officeClosingRules = {};
let officeIdsToLoad = [];
const selectedOfficeId = document.getElementById('officeFilter').value;
if (selectedOfficeId) {
officeIdsToLoad = [selectedOfficeId];
} else if (currentUser.role === 'employee' || (currentUser.role === 'manager' && currentUser.office_id)) {
officeIdsToLoad = [currentUser.office_id];
} else if (offices.length > 0) {
// Admin viewing all or Manager with access to list
officeIdsToLoad = offices.map(o => o.id);
}
if (officeIdsToLoad.length === 0) return;
// Fetch in parallel
const promises = officeIdsToLoad.map(async (oid) => {
try {
const [weeklyRes, specificRes] = await Promise.all([
api.get(`/api/offices/${oid}/weekly-closing-days`),
api.get(`/api/offices/${oid}/closing-days`)
]);
officeClosingRules[oid] = { weekly: [], specific: [] };
if (weeklyRes && weeklyRes.ok) {
const days = await weeklyRes.json();
officeClosingRules[oid].weekly = days.map(d => d.weekday);
}
if (specificRes && specificRes.ok) {
officeClosingRules[oid].specific = await specificRes.json();
// OPTIMIZATION: Pre-calculate all specific closed dates into a Set
const closedSet = new Set();
officeClosingRules[oid].specific.forEach(range => {
let start = new Date(range.date);
let end = range.end_date ? new Date(range.end_date) : new Date(range.date);
// Normalize to noon to avoid timezone issues when stepping
start.setHours(12, 0, 0, 0);
end.setHours(12, 0, 0, 0);
let current = new Date(start);
while (current <= end) {
closedSet.add(utils.formatDate(current));
current.setDate(current.getDate() + 1);
}
});
officeClosingRules[oid].closedDatesSet = closedSet;
}
} catch (e) {
console.error(`Error loading closing days for office ${oid}:`, e);
}
});
await Promise.all(promises);
}
function renderCalendar() {
const header = document.getElementById('calendarHeader');
const body = document.getElementById('calendarBody');
@@ -132,8 +241,8 @@ function renderCalendar() {
const dayCount = Math.round((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
// Build header row
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
let headerHtml = '<th>Name</th><th>Manager</th>';
const dayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
let headerHtml = '<th>Nome</th><th>Ufficio</th>';
for (let i = 0; i < dayCount; i++) {
const date = new Date(startDate);
@@ -141,13 +250,19 @@ function renderCalendar() {
const dayOfWeek = date.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const isHoliday = utils.isItalianHoliday(date);
const isToday = date.toDateString() === new Date().toDateString();
const isToday = utils.formatDate(date) === utils.formatDate(new Date());
let classes = [];
if (isWeekend) classes.push('weekend');
if (isHoliday) classes.push('holiday');
if (isToday) classes.push('today');
if (isToday) classes.push('today');
// Header doesn't show closed status in multi-office view
// unless we want to check if ALL are closed?
// For now, simpler to leave header clean.
headerHtml += `<th class="${classes.join(' ')}">
<div>${dayNames[dayOfWeek].charAt(0)}</div>
<div class="day-number">${date.getDate()}</div>
@@ -157,7 +272,7 @@ function renderCalendar() {
// Build body rows
if (teamData.length === 0) {
body.innerHTML = `<tr><td colspan="${dayCount + 2}" class="text-center">No team members found</td></tr>`;
body.innerHTML = `<tr><td colspan="${dayCount + 2}" class="text-center">Nessun membro del team trovato</td></tr>`;
return;
}
@@ -165,7 +280,7 @@ function renderCalendar() {
teamData.forEach(member => {
bodyHtml += `<tr>
<td class="member-name">${member.name || 'Unknown'}</td>
<td class="member-manager">${member.manager_name || '-'}</td>`;
<td class="member-manager">${member.office_name || '-'}</td>`;
for (let i = 0; i < dayCount; i++) {
const date = new Date(startDate);
@@ -179,7 +294,7 @@ function renderCalendar() {
const parkingKey = `${member.id}_${dateStr}`;
const parkingSpot = parkingDataLookup[parkingKey];
const hasParking = member.parking_dates && member.parking_dates.includes(dateStr);
const isToday = date.toDateString() === new Date().toDateString();
const isToday = dateStr === utils.formatDate(new Date());
let cellClasses = ['calendar-cell'];
if (isWeekend) cellClasses.push('weekend');
@@ -187,6 +302,47 @@ function renderCalendar() {
if (isToday) cellClasses.push('today');
if (presence) cellClasses.push(`status-${presence.status}`);
if (isToday) cellClasses.push('today');
if (presence) cellClasses.push(`status-${presence.status}`);
// Optimized closing day check
// Pre-calculate loop-invariant sets outside if not already done, but here we do it per-cell because of date dependency?
// BETTER: We should pre-calculate a "closedMap" for the viewed range for each office?
// OR: Just optimize the inner check.
// Optimization: Create a lookup string for the current date once
// (Already have dateStr)
const memberRules = officeClosingRules[member.office_id];
let isClosed = false;
if (memberRules) {
// Check weekly
if (memberRules.weekly.includes(dayOfWeek)) {
isClosed = true;
} else if (memberRules.specific && memberRules.specific.length > 0) {
// Check specific
// Optimization: Use the string date lookup if we had a Set, but we have ranges.
// We can optimize by converting ranges to Sets ONCE when loading data,
// OR just stick to this check if N is small.
// Given the "optimization" task, let's just make sure we don't do new Date() inside.
// The `specific` array contains objects with `date` and `end_date` strings.
// We can compare strings directly if format is YYYY-MM-DD and we are careful.
// Optimization: check if dateStr is in a Set of closed dates for this office?
// Let's implement the Set lookup logic in `loadClosingData` or `renderCalendar` start.
// For now, let's assume `memberRules.closedDatesSet` exists.
if (memberRules.closedDatesSet && memberRules.closedDatesSet.has(dateStr)) {
isClosed = true;
}
}
}
if (isClosed) {
cellClasses.push('closed');
}
// Show parking badge instead of just 'P'
let parkingBadge = '';
if (hasParking) {
@@ -194,7 +350,7 @@ function renderCalendar() {
parkingBadge = `<span class="parking-badge-sm">${spotName}</span>`;
}
bodyHtml += `<td class="${cellClasses.join(' ')}" data-user-id="${member.id}" data-date="${dateStr}" data-user-name="${member.name || ''}">${parkingBadge}</td>`;
bodyHtml += `<td class="${cellClasses.join(' ')}" data-user-id="${member.id}" data-date="${dateStr}" data-user-name="${member.name || ''}" ${isClosed ? 'data-closed="true"' : ''}>${parkingBadge}</td>`;
}
bodyHtml += '</tr>';
@@ -205,12 +361,14 @@ function renderCalendar() {
if (currentUser.role === 'admin' || currentUser.role === 'manager') {
body.querySelectorAll('.calendar-cell').forEach(cell => {
cell.style.cursor = 'pointer';
cell.addEventListener('click', () => {
const userId = cell.dataset.userId;
const date = cell.dataset.date;
const userName = cell.dataset.userName;
openDayModal(userId, date, userName);
});
if (cell.dataset.closed !== 'true') {
cell.addEventListener('click', () => {
const userId = cell.dataset.userId;
const date = cell.dataset.date;
const userName = cell.dataset.userName;
openDayModal(userId, date, userName);
});
}
});
}
}
@@ -219,129 +377,107 @@ function openDayModal(userId, dateStr, userName) {
selectedUserId = userId;
selectedDate = dateStr;
const modal = document.getElementById('dayModal');
document.getElementById('dayModalTitle').textContent = utils.formatDateDisplay(dateStr);
document.getElementById('dayModalUser').textContent = userName;
// Find current status and parking
const member = teamData.find(m => m.id === userId);
const presence = member?.presences.find(p => p.date === dateStr);
const parkingKey = `${userId}_${dateStr}`;
const parkingSpot = parkingDataLookup[parkingKey];
const assignmentId = parkingAssignmentLookup[parkingKey];
currentAssignmentId = assignmentId; // Ensure this is set for modal logic
// Highlight current status
document.querySelectorAll('#dayModal .status-btn').forEach(btn => {
const status = btn.dataset.status;
if (presence && presence.status === status) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
const parkingObj = assignmentId ? {
id: assignmentId,
spot_display_name: parkingSpot,
spot_id: parkingSpot
} : null;
ModalLogic.openModal({
dateStr,
userName,
presence,
parking: parkingObj,
userId
});
// Update parking section
const parkingSection = document.getElementById('parkingSection');
const parkingInfo = document.getElementById('parkingInfo');
if (parkingSpot) {
parkingSection.style.display = 'block';
parkingInfo.innerHTML = `<strong>Parking:</strong> Spot ${parkingSpot}`;
currentAssignmentId = assignmentId;
} else {
parkingSection.style.display = 'none';
currentAssignmentId = null;
}
modal.style.display = 'flex';
}
async function markPresence(status) {
if (!selectedUserId || !selectedDate) return;
async function handleMarkPresence(status, date, userId) {
// userId passed from ModalLogic if provided, or use selectedUserId
const targetUserId = userId || selectedUserId;
if (!targetUserId) return;
const response = await api.post('/api/presence/admin/mark', {
user_id: selectedUserId,
date: selectedDate,
user_id: targetUserId,
date: date,
status: status
});
if (response && response.ok) {
document.getElementById('dayModal').style.display = 'none';
ModalLogic.closeModal();
await loadTeamData();
renderCalendar();
} else {
const error = await response.json();
alert(error.detail || 'Failed to mark presence');
alert(error.detail || 'Impossibile segnare la presenza');
}
}
async function clearPresence() {
if (!selectedUserId || !selectedDate) return;
if (!confirm('Clear presence for this date?')) return;
async function handleClearPresence(date, userId) {
const targetUserId = userId || selectedUserId;
if (!targetUserId) return;
const response = await api.delete(`/api/presence/admin/${selectedUserId}/${selectedDate}`);
// confirm is not needed here if ModalLogic doesn't mandate it, but keeping logic
// ModalLogic buttons usually trigger this directly.
const response = await api.delete(`/api/presence/admin/${targetUserId}/${date}`);
if (response && response.ok) {
document.getElementById('dayModal').style.display = 'none';
ModalLogic.closeModal();
await loadTeamData();
renderCalendar();
}
}
async function openReassignModal() {
if (!currentAssignmentId) return;
async function handleReleaseParking(assignmentId) {
if (!confirm('Rilasciare il parcheggio per questa data?')) return;
// Load eligible users
const response = await api.get(`/api/parking/eligible-users/${currentAssignmentId}`);
if (!response || !response.ok) {
const error = await response.json();
alert(error.detail || 'Failed to load eligible users');
return;
}
// Note: Admin endpoint for releasing ANY spot vs "my spot"
// Since we are admin/manager here, we might need a general release endpoint or use reassign with null?
// The current 'release_my_spot' is only for self.
// 'reassign_spot' with null user_id is the way for admins.
const users = await response.json();
const select = document.getElementById('reassignUser');
select.innerHTML = '<option value="">Select user...</option>';
const response = await api.post('/api/parking/reassign-spot', {
assignment_id: assignmentId,
new_user_id: null // Release
});
if (users.length === 0) {
select.innerHTML = '<option value="">No eligible users available</option>';
if (response && response.ok) {
ModalLogic.closeModal();
await loadTeamData();
renderCalendar();
} else {
users.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.name;
select.appendChild(option);
});
const error = await response.json();
alert(error.detail || 'Impossibile rilasciare il parcheggio');
}
// Get spot info
const parkingKey = `${selectedUserId}_${selectedDate}`;
const spotName = parkingDataLookup[parkingKey] || 'Unknown';
document.getElementById('reassignSpotInfo').textContent = `Spot ${spotName}`;
document.getElementById('dayModal').style.display = 'none';
document.getElementById('reassignModal').style.display = 'flex';
}
async function confirmReassign() {
const newUserId = document.getElementById('reassignUser').value;
if (!currentAssignmentId || !newUserId) {
alert('Please select a user');
async function handleReassignParking(assignmentId, newUserId) {
if (!assignmentId || !newUserId) {
alert('Seleziona un utente');
return;
}
const response = await api.post('/api/parking/reassign-spot', {
assignment_id: currentAssignmentId,
assignment_id: assignmentId,
new_user_id: newUserId
});
if (response && response.ok) {
await loadTeamData();
renderCalendar();
document.getElementById('reassignModal').style.display = 'none';
ModalLogic.closeModal();
} else {
const error = await response.json();
alert(error.detail || 'Failed to reassign parking spot');
alert(error.detail || 'Impossibile riassegnare il parcheggio');
}
}
@@ -375,40 +511,20 @@ function setupEventListeners() {
currentStartDate = new Date(currentStartDate.getFullYear(), currentStartDate.getMonth(), 1);
} else {
// Set to current week start
const weekStartDay = currentUser.week_start_day || 0;
const weekStartDay = currentUser.week_start_day || 1;
currentStartDate = utils.getWeekStart(new Date(), weekStartDay);
}
await loadTeamData();
renderCalendar();
});
// Manager filter
document.getElementById('managerFilter').addEventListener('change', async () => {
// Office filter
document.getElementById('officeFilter').addEventListener('change', async () => {
updateOfficeDisplay(); // Update label on change
await loadTeamData();
renderCalendar();
});
// Day modal
document.getElementById('closeDayModal').addEventListener('click', () => {
document.getElementById('dayModal').style.display = 'none';
});
document.querySelectorAll('#dayModal .status-btn').forEach(btn => {
btn.addEventListener('click', () => markPresence(btn.dataset.status));
});
document.getElementById('clearDayBtn').addEventListener('click', clearPresence);
document.getElementById('reassignParkingBtn').addEventListener('click', openReassignModal);
utils.setupModalClose('dayModal');
// Reassign modal
document.getElementById('closeReassignModal').addEventListener('click', () => {
document.getElementById('reassignModal').style.display = 'none';
});
document.getElementById('cancelReassign').addEventListener('click', () => {
document.getElementById('reassignModal').style.display = 'none';
});
document.getElementById('confirmReassign').addEventListener('click', confirmReassign);
utils.setupModalClose('reassignModal');
}

View File

@@ -1,370 +1,383 @@
/**
* Team Rules Page
* Manage closing days, parking guarantees, and exclusions
*
* Rules are set at manager level for their parking pool.
* Manage closing days, guarantees, and exclusions
* Office-centric model
*/
let currentUser = null;
let selectedManagerId = null;
let managerUsers = [];
let offices = [];
let currentOfficeId = null;
let officeUsers = [];
let currentWeeklyClosingDays = [];
document.addEventListener('DOMContentLoaded', async () => {
currentUser = await api.requireAuth();
if (!currentUser) return;
// Only managers and admins can access
if (currentUser.role === 'employee') {
// Only admins and managers can access this page
if (currentUser.role !== 'admin' && currentUser.role !== 'manager') {
window.location.href = '/presence';
return;
}
await loadManagers();
await loadOffices();
setupEventListeners();
});
async function loadManagers() {
const response = await api.get('/api/managers');
if (response && response.ok) {
const managers = await response.json();
const select = document.getElementById('managerSelect');
async function loadOffices() {
const select = document.getElementById('officeSelect');
const card = document.getElementById('officeSelectionCard');
// Filter to managers this user can see
let filteredManagers = managers;
// Only Admins can see the office selector
if (currentUser.role !== 'admin') {
if (card) card.style.display = 'none';
}
const response = await api.get('/api/offices');
if (response && response.ok) {
offices = await response.json();
let filteredOffices = offices;
if (currentUser.role === 'manager') {
// Manager only sees themselves
filteredManagers = managers.filter(m => m.id === currentUser.id);
// Manager only sees their own office
if (currentUser.office_id) {
filteredOffices = offices.filter(o => o.id === currentUser.office_id);
} else {
filteredOffices = [];
}
}
// Show managers in dropdown
let totalManagers = 0;
let firstManagerId = null;
filteredManagers.forEach(manager => {
filteredOffices.forEach(office => {
const option = document.createElement('option');
option.value = manager.id;
// Show manager name with user count and parking quota
const userCount = manager.managed_user_count || 0;
const quota = manager.parking_quota || 0;
option.textContent = `${manager.name} (${userCount} users, ${quota} spots)`;
option.value = office.id;
option.textContent = office.name;
select.appendChild(option);
totalManagers++;
if (!firstManagerId) firstManagerId = manager.id;
});
// Auto-select if only one manager
if (totalManagers === 1 && firstManagerId) {
select.value = firstManagerId;
await selectManager(firstManagerId);
// Auto-select for managers
if (currentUser.role === 'manager' && filteredOffices.length === 1) {
select.value = filteredOffices[0].id;
loadOfficeRules(filteredOffices[0].id);
}
}
}
async function selectManager(managerId) {
selectedManagerId = managerId;
if (!managerId) {
async function loadOfficeRules(officeId) {
if (!officeId) {
document.getElementById('rulesContent').style.display = 'none';
document.getElementById('noManagerMessage').style.display = 'block';
document.getElementById('noOfficeMessage').style.display = 'block';
return;
}
currentOfficeId = officeId;
document.getElementById('rulesContent').style.display = 'block';
document.getElementById('noManagerMessage').style.display = 'none';
document.getElementById('noOfficeMessage').style.display = 'none';
// Load users for this office (for dropdowns)
await loadOfficeUsers(officeId);
await Promise.all([
loadWeeklyClosingDays(),
loadClosingDays(),
loadGuarantees(),
loadExclusions(),
loadManagerUsers()
loadWeeklyClosingDays(officeId),
loadClosingDays(officeId),
loadGuarantees(officeId),
loadExclusions(officeId)
]);
}
async function loadWeeklyClosingDays() {
const response = await api.get(`/api/managers/${selectedManagerId}/weekly-closing-days`);
async function loadOfficeUsers(officeId) {
const response = await api.get(`/api/offices/${officeId}/users`);
if (response && response.ok) {
officeUsers = await response.json();
}
}
// Weekly Closing Days
async function loadWeeklyClosingDays(officeId) {
const response = await api.get(`/api/offices/${officeId}/weekly-closing-days`);
if (response && response.ok) {
const days = await response.json();
const weekdays = days.map(d => d.weekday);
currentWeeklyClosingDays = days;
const activeWeekdays = days.map(d => d.weekday);
// Update checkboxes
document.querySelectorAll('#weeklyClosingDays input[type="checkbox"]').forEach(cb => {
const weekday = parseInt(cb.dataset.weekday);
cb.checked = weekdays.includes(weekday);
cb.checked = activeWeekdays.includes(weekday);
});
}
}
async function loadManagerUsers() {
const response = await api.get(`/api/managers/${selectedManagerId}/users`);
if (response && response.ok) {
managerUsers = await response.json();
updateUserSelects();
async function saveWeeklyClosingDays() {
const btn = document.getElementById('saveWeeklyClosingDaysBtn');
if (!btn) return;
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Salvataggio...';
try {
const promises = [];
const checkboxes = document.querySelectorAll('#weeklyClosingDays input[type="checkbox"]');
for (const cb of checkboxes) {
const weekday = parseInt(cb.dataset.weekday);
const isChecked = cb.checked;
const existingEntry = currentWeeklyClosingDays.find(d => d.weekday === weekday);
if (isChecked && !existingEntry) {
// Add
promises.push(api.post(`/api/offices/${currentOfficeId}/weekly-closing-days`, { weekday }));
} else if (!isChecked && existingEntry) {
// Remove
promises.push(api.delete(`/api/offices/${currentOfficeId}/weekly-closing-days/${existingEntry.id}`));
}
}
await Promise.all(promises);
utils.showMessage('Giorni di chiusura aggiornati', 'success');
await loadWeeklyClosingDays(currentOfficeId);
} catch (error) {
console.error(error);
utils.showMessage('Errore durante il salvataggio', 'error');
} finally {
btn.disabled = false;
btn.textContent = originalText;
}
}
function updateUserSelects() {
['guaranteeUser', 'exclusionUser'].forEach(selectId => {
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">Select user...</option>';
managerUsers.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.name;
select.appendChild(option);
});
});
}
async function loadClosingDays() {
const response = await api.get(`/api/managers/${selectedManagerId}/closing-days`);
// Closing Days
async function loadClosingDays(officeId) {
const response = await api.get(`/api/offices/${officeId}/closing-days`);
const container = document.getElementById('closingDaysList');
if (response && response.ok) {
const days = await response.json();
if (days.length === 0) {
container.innerHTML = '';
container.innerHTML = '<p class="text-muted">Nessun giorno di chiusura specifico.</p>';
return;
}
container.innerHTML = days.map(day => `
<div class="rule-item">
<div class="rule-info">
<span class="rule-date">${utils.formatDateDisplay(day.date)}</span>
${day.reason ? `<span class="rule-reason">${day.reason}</span>` : ''}
<strong>${utils.formatDateDisplay(day.date)}${day.end_date ? ' - ' + utils.formatDateDisplay(day.end_date) : ''}</strong>
${day.reason ? `<span class="rule-note">${day.reason}</span>` : ''}
</div>
<button class="btn-icon btn-danger" onclick="deleteClosingDay('${day.id}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
&times;
</button>
</div>
`).join('');
}
}
function formatDateRange(startDate, endDate) {
if (!startDate && !endDate) return '';
if (startDate && !endDate) return `From ${utils.formatDateDisplay(startDate)}`;
if (!startDate && endDate) return `Until ${utils.formatDateDisplay(endDate)}`;
return `${utils.formatDateDisplay(startDate)} - ${utils.formatDateDisplay(endDate)}`;
async function addClosingDay(data) {
const response = await api.post(`/api/offices/${currentOfficeId}/closing-days`, data);
if (response && response.ok) {
await loadClosingDays(currentOfficeId);
document.getElementById('closingDayModal').style.display = 'none';
document.getElementById('closingDayForm').reset();
} else {
const error = await response.json();
alert(error.detail || 'Impossibile aggiungere il giorno di chiusura');
}
}
async function loadGuarantees() {
const response = await api.get(`/api/managers/${selectedManagerId}/guarantees`);
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) {
await loadClosingDays(currentOfficeId);
}
}
// Guarantees
async function loadGuarantees(officeId) {
const response = await api.get(`/api/offices/${officeId}/guarantees`);
const container = document.getElementById('guaranteesList');
if (response && response.ok) {
const guarantees = await response.json();
if (guarantees.length === 0) {
container.innerHTML = '';
container.innerHTML = '<p class="text-muted">Nessuna garanzia di parcheggio attiva.</p>';
return;
}
container.innerHTML = guarantees.map(g => {
const dateRange = formatDateRange(g.start_date, g.end_date);
return `
container.innerHTML = guarantees.map(g => `
<div class="rule-item">
<div class="rule-info">
<span class="rule-name">${g.user_name}</span>
${dateRange ? `<span class="rule-date-range">${dateRange}</span>` : ''}
<strong>${g.user_name || 'Utente sconosciuto'}</strong>
<span class="rule-dates">
${g.start_date ? 'Dal ' + utils.formatDateDisplay(g.start_date) : 'Da sempre'}
${g.end_date ? ' al ' + utils.formatDateDisplay(g.end_date) : ''}
</span>
${g.notes ? `<span class="rule-note">${g.notes}</span>` : ''}
</div>
<button class="btn-icon btn-danger" onclick="deleteGuarantee('${g.id}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
&times;
</button>
</div>
`}).join('');
`).join('');
}
}
async function loadExclusions() {
const response = await api.get(`/api/managers/${selectedManagerId}/exclusions`);
const container = document.getElementById('exclusionsList');
async function addGuarantee(data) {
const response = await api.post(`/api/offices/${currentOfficeId}/guarantees`, data);
if (response && response.ok) {
const exclusions = await response.json();
if (exclusions.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = exclusions.map(e => {
const dateRange = formatDateRange(e.start_date, e.end_date);
return `
<div class="rule-item">
<div class="rule-info">
<span class="rule-name">${e.user_name}</span>
${dateRange ? `<span class="rule-date-range">${dateRange}</span>` : ''}
</div>
<button class="btn-icon btn-danger" onclick="deleteExclusion('${e.id}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
`}).join('');
}
}
// Delete functions
async function deleteClosingDay(id) {
if (!confirm('Delete this closing day?')) return;
const response = await api.delete(`/api/managers/${selectedManagerId}/closing-days/${id}`);
if (response && response.ok) {
await loadClosingDays();
await loadGuarantees(currentOfficeId);
document.getElementById('guaranteeModal').style.display = 'none';
document.getElementById('guaranteeForm').reset();
} else {
const error = await response.json();
alert(error.detail || 'Impossibile aggiungere la garanzia');
}
}
async function deleteGuarantee(id) {
if (!confirm('Remove this parking guarantee?')) return;
const response = await api.delete(`/api/managers/${selectedManagerId}/guarantees/${id}`);
if (!confirm('Eliminare questa garanzia?')) return;
const response = await api.delete(`/api/offices/${currentOfficeId}/guarantees/${id}`);
if (response && response.ok) {
await loadGuarantees();
await loadGuarantees(currentOfficeId);
}
}
// Exclusions
async function loadExclusions(officeId) {
const response = await api.get(`/api/offices/${officeId}/exclusions`);
const container = document.getElementById('exclusionsList');
if (response && response.ok) {
const exclusions = await response.json();
if (exclusions.length === 0) {
container.innerHTML = '<p class="text-muted">Nessuna esclusione attiva.</p>';
return;
}
container.innerHTML = exclusions.map(e => `
<div class="rule-item">
<div class="rule-info">
<strong>${e.user_name || 'Utente sconosciuto'}</strong>
<span class="rule-dates">
${e.start_date ? 'Dal ' + utils.formatDateDisplay(e.start_date) : 'Da sempre'}
${e.end_date ? ' al ' + utils.formatDateDisplay(e.end_date) : ''}
</span>
${e.notes ? `<span class="rule-note">${e.notes}</span>` : ''}
</div>
<button class="btn-icon btn-danger" onclick="deleteExclusion('${e.id}')">
&times;
</button>
</div>
`).join('');
}
}
async function addExclusion(data) {
const response = await api.post(`/api/offices/${currentOfficeId}/exclusions`, data);
if (response && response.ok) {
await loadExclusions(currentOfficeId);
document.getElementById('exclusionModal').style.display = 'none';
document.getElementById('exclusionForm').reset();
} else {
const error = await response.json();
alert(error.detail || 'Impossibile aggiungere l\'esclusione');
}
}
async function deleteExclusion(id) {
if (!confirm('Remove this parking exclusion?')) return;
const response = await api.delete(`/api/managers/${selectedManagerId}/exclusions/${id}`);
if (!confirm('Eliminare questa esclusione?')) return;
const response = await api.delete(`/api/offices/${currentOfficeId}/exclusions/${id}`);
if (response && response.ok) {
await loadExclusions();
await loadExclusions(currentOfficeId);
}
}
function setupEventListeners() {
// Manager selection
document.getElementById('managerSelect').addEventListener('change', (e) => {
selectManager(e.target.value);
});
function populateUserSelects() {
const selects = ['guaranteeUser', 'exclusionUser'];
selects.forEach(id => {
const select = document.getElementById(id);
const currentVal = select.value;
select.innerHTML = '<option value="">Seleziona utente...</option>';
// Weekly closing day checkboxes
document.querySelectorAll('#weeklyClosingDays input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', async (e) => {
const weekday = parseInt(e.target.dataset.weekday);
if (e.target.checked) {
// Add weekly closing day
const response = await api.post(`/api/managers/${selectedManagerId}/weekly-closing-days`, { weekday });
if (!response || !response.ok) {
e.target.checked = false;
const error = await response.json();
alert(error.detail || 'Failed to add weekly closing day');
}
} else {
// Remove weekly closing day - need to find the ID first
const getResponse = await api.get(`/api/managers/${selectedManagerId}/weekly-closing-days`);
if (getResponse && getResponse.ok) {
const days = await getResponse.json();
const day = days.find(d => d.weekday === weekday);
if (day) {
const deleteResponse = await api.delete(`/api/managers/${selectedManagerId}/weekly-closing-days/${day.id}`);
if (!deleteResponse || !deleteResponse.ok) {
e.target.checked = true;
}
}
}
}
officeUsers.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.name;
select.appendChild(option);
});
});
// Modal openers
document.getElementById('addClosingDayBtn').addEventListener('click', () => {
document.getElementById('closingDayForm').reset();
document.getElementById('closingDayModal').style.display = 'flex';
if (currentVal) select.value = currentVal;
});
document.getElementById('addGuaranteeBtn').addEventListener('click', () => {
document.getElementById('guaranteeForm').reset();
document.getElementById('guaranteeModal').style.display = 'flex';
});
document.getElementById('addExclusionBtn').addEventListener('click', () => {
document.getElementById('exclusionForm').reset();
document.getElementById('exclusionModal').style.display = 'flex';
});
// Modal closers
['closeClosingDayModal', 'cancelClosingDay'].forEach(id => {
document.getElementById(id).addEventListener('click', () => {
document.getElementById('closingDayModal').style.display = 'none';
});
});
['closeGuaranteeModal', 'cancelGuarantee'].forEach(id => {
document.getElementById(id).addEventListener('click', () => {
document.getElementById('guaranteeModal').style.display = 'none';
});
});
['closeExclusionModal', 'cancelExclusion'].forEach(id => {
document.getElementById(id).addEventListener('click', () => {
document.getElementById('exclusionModal').style.display = 'none';
});
});
// Form submissions
document.getElementById('closingDayForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
date: document.getElementById('closingDate').value,
reason: document.getElementById('closingReason').value || null
};
const response = await api.post(`/api/managers/${selectedManagerId}/closing-days`, data);
if (response && response.ok) {
document.getElementById('closingDayModal').style.display = 'none';
await loadClosingDays();
} else {
const error = await response.json();
alert(error.detail || 'Failed to add closing day');
}
});
document.getElementById('guaranteeForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
user_id: document.getElementById('guaranteeUser').value,
start_date: document.getElementById('guaranteeStartDate').value || null,
end_date: document.getElementById('guaranteeEndDate').value || null
};
const response = await api.post(`/api/managers/${selectedManagerId}/guarantees`, data);
if (response && response.ok) {
document.getElementById('guaranteeModal').style.display = 'none';
await loadGuarantees();
} else {
const error = await response.json();
alert(error.detail || 'Failed to add guarantee');
}
});
document.getElementById('exclusionForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
user_id: document.getElementById('exclusionUser').value,
start_date: document.getElementById('exclusionStartDate').value || null,
end_date: document.getElementById('exclusionEndDate').value || null
};
const response = await api.post(`/api/managers/${selectedManagerId}/exclusions`, data);
if (response && response.ok) {
document.getElementById('exclusionModal').style.display = 'none';
await loadExclusions();
} else {
const error = await response.json();
alert(error.detail || 'Failed to add exclusion');
}
});
// Modal background clicks
utils.setupModalClose('closingDayModal');
utils.setupModalClose('guaranteeModal');
utils.setupModalClose('exclusionModal');
}
// Make delete functions globally accessible
function setupEventListeners() {
// Office select
document.getElementById('officeSelect').addEventListener('change', (e) => {
loadOfficeRules(e.target.value);
});
// Save Weekly closing days
const saveBtn = document.getElementById('saveWeeklyClosingDaysBtn');
if (saveBtn) {
saveBtn.addEventListener('click', saveWeeklyClosingDays);
}
// Modals
const modals = [
{ id: 'closingDayModal', btn: 'addClosingDayBtn', close: 'closeClosingDayModal', cancel: 'cancelClosingDay' },
{ id: 'guaranteeModal', btn: 'addGuaranteeBtn', close: 'closeGuaranteeModal', cancel: 'cancelGuarantee' },
{ id: 'exclusionModal', btn: 'addExclusionBtn', close: 'closeExclusionModal', cancel: 'cancelExclusion' }
];
modals.forEach(m => {
document.getElementById(m.btn).addEventListener('click', () => {
if (m.id !== 'closingDayModal') populateUserSelects();
document.getElementById(m.id).style.display = 'flex';
});
document.getElementById(m.close).addEventListener('click', () => {
document.getElementById(m.id).style.display = 'none';
});
document.getElementById(m.cancel).addEventListener('click', () => {
document.getElementById(m.id).style.display = 'none';
});
utils.setupModalClose(m.id);
});
// Forms
document.getElementById('closingDayForm').addEventListener('submit', (e) => {
e.preventDefault();
addClosingDay({
date: document.getElementById('closingDate').value,
end_date: document.getElementById('closingEndDate').value || null,
reason: document.getElementById('closingReason').value || null
});
});
document.getElementById('guaranteeForm').addEventListener('submit', (e) => {
e.preventDefault();
addGuarantee({
user_id: document.getElementById('guaranteeUser').value,
start_date: document.getElementById('guaranteeStartDate').value || null,
end_date: document.getElementById('guaranteeEndDate').value || null,
notes: document.getElementById('guaranteeNotes').value || null
});
});
document.getElementById('exclusionForm').addEventListener('submit', (e) => {
e.preventDefault();
addExclusion({
user_id: document.getElementById('exclusionUser').value,
start_date: document.getElementById('exclusionStartDate').value || null,
end_date: document.getElementById('exclusionEndDate').value || null,
notes: document.getElementById('exclusionNotes').value || null
});
});
}
// Global functions
window.deleteClosingDay = deleteClosingDay;
window.deleteGuarantee = deleteGuarantee;
window.deleteExclusion = deleteExclusion;

View File

@@ -97,7 +97,7 @@ function formatDate(date) {
*/
function formatDateDisplay(dateStr) {
const date = new Date(dateStr + 'T12:00:00');
return date.toLocaleDateString('en-US', {
return date.toLocaleDateString('it-IT', {
weekday: 'short',
month: 'short',
day: 'numeric'
@@ -109,8 +109,8 @@ function formatDateDisplay(dateStr) {
*/
function getMonthName(month) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno',
'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'
];
return months[month];
}
@@ -119,7 +119,7 @@ function getMonthName(month) {
* Get day name
*/
function getDayName(dayIndex) {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const days = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
return days[dayIndex];
}
@@ -133,7 +133,7 @@ function getDaysInMonth(year, month) {
/**
* Get start of week for a date
*/
function getWeekStart(date, weekStartDay = 0) {
function getWeekStart(date, weekStartDay = 1) {
const d = new Date(date);
const day = d.getDay();
const diff = (day - weekStartDay + 7) % 7;
@@ -146,7 +146,7 @@ function getWeekStart(date, weekStartDay = 0) {
* Format date as short display (e.g., "Nov 26")
*/
function formatDateShort(date) {
return date.toLocaleDateString('en-US', {
return date.toLocaleDateString('it-IT', {
month: 'short',
day: 'numeric'
});
@@ -163,12 +163,14 @@ function showMessage(message, type = 'success', duration = 3000) {
toastContainer.id = 'toastContainer';
toastContainer.style.cssText = `
position: fixed;
top: 1rem;
right: 1rem;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 1rem;
align-items: center;
`;
document.body.appendChild(toastContainer);
}
@@ -176,17 +178,21 @@ function showMessage(message, type = 'success', duration = 3000) {
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;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
animation: slideInBottom 0.3s ease;
font-size: 1.1rem;
font-weight: 500;
min-width: 300px;
text-align: center;
`;
toast.textContent = message;
toastContainer.appendChild(toast);
if (duration > 0) {
setTimeout(() => {
toast.style.animation = 'slideOut 0.2s ease';
toast.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => toast.remove(), 200);
}, duration);
}
@@ -196,14 +202,7 @@ function showMessage(message, type = 'success', duration = 3000) {
* 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';
}
});
}
// Behavior disabled: clicking outside does not close modal
}
// Export utilities