Primo commit
This commit is contained in:
BIN
frontend/assets/parking-map.png
Normal file
BIN
frontend/assets/parking-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 786 KiB |
@@ -30,7 +30,9 @@
|
||||
/* ============================================================================
|
||||
Reset & Base
|
||||
============================================================================ */
|
||||
*, *::before, *::after {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -54,7 +56,9 @@ button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
@@ -431,11 +435,12 @@ input, select, textarea {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.modal-small {
|
||||
max-width: 360px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@@ -612,16 +617,23 @@ input, select, textarea {
|
||||
|
||||
.calendar-day .parking-badge {
|
||||
position: absolute;
|
||||
bottom: 0.25rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 6px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
font-size: 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
padding: 0.3rem 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #93c5fd;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
transform: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
@@ -644,6 +656,28 @@ input, select, textarea {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Closed Day */
|
||||
.calendar-day.closed {
|
||||
background: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.calendar-day.closed:hover {
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
.calendar-day.closed .day-number {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.team-calendar td.closed {
|
||||
background: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Legend */
|
||||
.legend {
|
||||
display: flex;
|
||||
@@ -975,11 +1009,11 @@ input, select, textarea {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
.toggle-switch input:checked+.toggle-slider {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
.toggle-switch input:checked+.toggle-slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
@@ -1747,3 +1781,24 @@ input, select, textarea {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toast Animations */
|
||||
@keyframes slideInBottom {
|
||||
from {
|
||||
transform: translate(-50%, 100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
167
frontend/js/admin-offices.js
Normal file
167
frontend/js/admin-offices.js
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
188
frontend/js/modal-logic.js
Normal 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';
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
226
frontend/js/parking-settings.js
Normal file
226
frontend/js/parking-settings.js
Normal 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');
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
×
|
||||
</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>
|
||||
×
|
||||
</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}')">
|
||||
×
|
||||
</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;
|
||||
|
||||
@@ -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
|
||||
|
||||
113
frontend/pages/admin-offices.html
Normal file
113
frontend/pages/admin-offices.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gestione Uffici - Parking Manager</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Gestione Uffici</h2>
|
||||
<div class="header-actions">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="card">
|
||||
<div style="padding-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 style="margin: 0;">Lista Uffici</h3>
|
||||
<button class="btn btn-dark" id="addOfficeBtn">Nuovo Ufficio</button>
|
||||
</div>
|
||||
<div class="data-table-container">
|
||||
<table class="data-table" id="officesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Quota Posti</th>
|
||||
<th>Prefisso</th>
|
||||
<th>Utenti</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="officesBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Office Modal -->
|
||||
<div class="modal" id="officeModal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="officeModalTitle">Nuovo Ufficio</h3>
|
||||
<button class="modal-close" id="closeOfficeModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="officeForm">
|
||||
<input type="hidden" id="officeId">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="officeName">Nome Ufficio</label>
|
||||
<input type="text" id="officeName" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="officeQuota">Quota Parcheggio</label>
|
||||
<input type="number" id="officeQuota" min="0" value="0" required>
|
||||
<small class="text-muted">Numero totale di posti auto assegnati a questo ufficio</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelOffice">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark" id="saveOfficeBtn">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/utils.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/admin-offices.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,31 +8,33 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Loading...</div>
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profile</a>
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Logout</button>
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,23 +42,33 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Manage Users</h2>
|
||||
<h2>Gestione Utenti</h2>
|
||||
<div class="header-actions">
|
||||
<input type="text" id="searchInput" class="form-input" placeholder="Search users...">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="card">
|
||||
<div style="padding-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3 style="margin: 0;">Lista Utenti</h3>
|
||||
<input type="text" id="searchInput" class="form-input" placeholder="Cerca utenti..."
|
||||
style="max-width: 300px;">
|
||||
</div>
|
||||
<div class="data-table-container">
|
||||
<table class="data-table" id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Manager</th>
|
||||
<th>Actions</th>
|
||||
<th class="sortable" data-sort="name" style="cursor: pointer;">Nome <span
|
||||
class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="email" style="cursor: pointer;">Email <span
|
||||
class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="role" style="cursor: pointer;">Ruolo <span
|
||||
class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="office_name" style="cursor: pointer;">Ufficio <span
|
||||
class="sort-icon"></span></th>
|
||||
<th class="sortable" data-sort="parking_ratio" style="cursor: pointer;">Punteggio <span
|
||||
class="sort-icon"></span></th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersBody"></tbody>
|
||||
@@ -69,7 +82,7 @@
|
||||
<div class="modal" id="userModal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="userModalTitle">Edit User</h3>
|
||||
<h3 id="userModalTitle">Modifica Utente</h3>
|
||||
<button class="modal-close" id="closeUserModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -78,54 +91,42 @@
|
||||
|
||||
<!-- LDAP notice -->
|
||||
<div id="ldapNotice" class="form-notice" style="display: none;">
|
||||
<small>This user is managed by LDAP. Some fields cannot be edited.</small>
|
||||
<small>Questo utente è gestito da LDAP. Alcuni campi non possono essere modificati.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editName">Name</label>
|
||||
<label for="editName">Nome</label>
|
||||
<input type="text" id="editName" required>
|
||||
<small id="nameHelp" class="text-muted" style="display: none;">Managed by LDAP</small>
|
||||
<small id="nameHelp" class="text-muted" style="display: none;">Gestito da LDAP</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editEmail">Email</label>
|
||||
<input type="email" id="editEmail" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editRole">Role</label>
|
||||
<label for="editRole">Ruolo</label>
|
||||
<select id="editRole" required>
|
||||
<option value="employee">Employee</option>
|
||||
<option value="employee">Dipendente</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<small id="roleHelp" class="text-muted" style="display: none;">Admin role is managed by LDAP group</small>
|
||||
<small id="roleHelp" class="text-muted" style="display: none;">Il ruolo admin è gestito dal
|
||||
gruppo LDAP</small>
|
||||
</div>
|
||||
<div class="form-group" id="managerGroup">
|
||||
<label for="editManager">Manager</label>
|
||||
<select id="editManager">
|
||||
<option value="">No manager</option>
|
||||
<div class="form-group" id="officeGroup">
|
||||
<label for="editOffice">Ufficio</label>
|
||||
<select id="editOffice">
|
||||
<option value="">Nessun ufficio</option>
|
||||
</select>
|
||||
<small class="text-muted">Who manages this user</small>
|
||||
<small class="text-muted">Ufficio di appartenenza</small>
|
||||
</div>
|
||||
|
||||
<!-- Manager-specific fields -->
|
||||
<div id="managerFields" style="display: none;">
|
||||
<hr>
|
||||
<h4>Manager Settings</h4>
|
||||
<div class="form-group">
|
||||
<label for="editQuota">Parking Quota</label>
|
||||
<input type="number" id="editQuota" min="0" value="0">
|
||||
<small class="text-muted">Number of parking spots this manager controls</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editPrefix">Spot Prefix</label>
|
||||
<input type="text" id="editPrefix" maxlength="2" placeholder="A, B, C...">
|
||||
<small class="text-muted">Letter prefix for parking spots (e.g., A for spots A1, A2...)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelUser">Cancel</button>
|
||||
<button type="submit" class="btn btn-dark">Save</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelUser">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark">Salva</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -137,4 +138,5 @@
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/admin-users.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -11,13 +11,13 @@
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<p>Manage team presence and parking assignments</p>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
<p>Gestisci la presenza del team e le assegnazioni dei parcheggi</p>
|
||||
</div>
|
||||
|
||||
<div id="authButtons" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<!-- Buttons will be populated by JavaScript based on auth mode -->
|
||||
<div class="loading">Loading...</div>
|
||||
<div class="loading">Caricamento...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,30 +56,30 @@
|
||||
if (config.login_url) {
|
||||
// Redirect to Authelia login with return URL
|
||||
const returnUrl = encodeURIComponent(window.location.origin + '/presence');
|
||||
buttons += `<a href="${config.login_url}?rd=${returnUrl}" class="btn btn-dark btn-full">Sign In</a>`;
|
||||
buttons += `<a href="${config.login_url}?rd=${returnUrl}" class="btn btn-dark btn-full">Accedi</a>`;
|
||||
} else {
|
||||
// No login URL configured - just try to access the app (Authelia will intercept)
|
||||
buttons += `<a href="/presence" class="btn btn-dark btn-full">Sign In</a>`;
|
||||
buttons += `<a href="/presence" class="btn btn-dark btn-full">Accedi</a>`;
|
||||
}
|
||||
|
||||
if (config.registration_url) {
|
||||
buttons += `<a href="${config.registration_url}" class="btn btn-secondary btn-full" target="_blank">Create Account</a>`;
|
||||
buttons += `<p class="auth-footer" style="margin-top: 0.5rem; text-align: center; font-size: 0.875rem; color: #666;">Registration requires admin approval</p>`;
|
||||
buttons += `<a href="${config.registration_url}" class="btn btn-secondary btn-full" target="_blank">Crea Account</a>`;
|
||||
buttons += `<p class="auth-footer" style="margin-top: 0.5rem; text-align: center; font-size: 0.875rem; color: #666;">La registrazione richiede l'approvazione dell'amministratore</p>`;
|
||||
}
|
||||
|
||||
buttonsDiv.innerHTML = buttons;
|
||||
} else {
|
||||
// Standalone mode: Local login and registration
|
||||
buttonsDiv.innerHTML = `
|
||||
<a href="/login" class="btn btn-dark btn-full">Sign In</a>
|
||||
<a href="/register" class="btn btn-secondary btn-full">Create Account</a>
|
||||
<a href="/login" class="btn btn-dark btn-full">Accedi</a>
|
||||
<a href="/register" class="btn btn-secondary btn-full">Crea Account</a>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to standalone mode
|
||||
buttonsDiv.innerHTML = `
|
||||
<a href="/login" class="btn btn-dark btn-full">Sign In</a>
|
||||
<a href="/register" class="btn btn-secondary btn-full">Create Account</a>
|
||||
<a href="/login" class="btn btn-dark btn-full">Accedi</a>
|
||||
<a href="/register" class="btn btn-secondary btn-full">Crea Account</a>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,12 +8,13 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Welcome Back</h1>
|
||||
<p>Sign in to your account</p>
|
||||
<h1>Bentornato</h1>
|
||||
<p>Accedi al tuo account</p>
|
||||
</div>
|
||||
|
||||
<div id="errorMessage"></div>
|
||||
@@ -26,11 +28,11 @@
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark btn-full">Sign In</button>
|
||||
<button type="submit" class="btn btn-dark btn-full">Accedi</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
Don't have an account? <a href="/register">Sign up</a>
|
||||
Non hai un account? <a href="/register">Registrati</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,4 +82,5 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
155
frontend/pages/parking-settings.html
Normal file
155
frontend/pages/parking-settings.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Office Settings - Parking Manager</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Impostazioni Ufficio</h2>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
|
||||
<!-- Office Selection Card (Admin Only) -->
|
||||
<div class="card" id="officeSelectionCard" style="margin-bottom: 1.5rem; display: none;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<label for="officeSelect" style="font-weight: 500; min-width: max-content;">Seleziona
|
||||
Ufficio:</label>
|
||||
<select id="officeSelect" class="form-select" style="flex: 1; max-width: 300px;">
|
||||
<option value="">Seleziona Ufficio</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settingsContent" style="display: none;">
|
||||
|
||||
<!-- Card 1: Batch Scheduling Settings -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Schedulazione Automatica</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="scheduleForm">
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<span>Abilita Assegnazione Batch</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="bookingWindowEnabled">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<small class="text-muted">Se abilitato, l'assegnazione dei posti avverrà solo dopo
|
||||
l'orario
|
||||
di cut-off del giorno precedente.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="cutoffTimeGroup">
|
||||
<label>Orario di Cut-off (Giorno Precedente)</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<select id="bookingWindowHour" style="width: 80px;">
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
<span>:</span>
|
||||
<select id="bookingWindowMinute" style="width: 80px;">
|
||||
<option value="0">00</option>
|
||||
<option value="15">15</option>
|
||||
<option value="30">30</option>
|
||||
<option value="45">45</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted">Le presenze inserite prima di questo orario saranno messe in
|
||||
attesa.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-dark">Salva Impostazioni</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Testing Tools -->
|
||||
<div class="card">
|
||||
<div class="card-header"
|
||||
style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3>Strumenti di Test</h3>
|
||||
<span class="badge badge-warning">Testing Only</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">Usa questi strumenti per verificare il
|
||||
funzionamento dell'assegnazione automatica.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Range di Date di Test</label>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<div>
|
||||
<small>Da:</small>
|
||||
<input type="date" id="testDateStart" class="form-control" style="width: 160px;">
|
||||
</div>
|
||||
<div>
|
||||
<small>A (incluso):</small>
|
||||
<input type="date" id="testDateEnd" class="form-control" style="width: 160px;">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">Lascia "A" vuoto per eseguire su un singolo giorno.</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<button id="runAllocationBtn" class="btn btn-primary">
|
||||
Esegui Assegnazione Ora
|
||||
</button>
|
||||
<button id="clearAssignmentsBtn" class="btn btn-danger">
|
||||
Elimina Tutte le Assegnazioni
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- End settingsContent -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/utils.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/parking-settings.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,31 +8,33 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Loading...</div>
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profile</a>
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Logout</button>
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,26 +42,36 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>My Presence</h2>
|
||||
<h2>Dashboard</h2>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-secondary" id="bulkMarkBtn">Bulk Mark</button>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="card presence-card">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h3>Calendario</h3>
|
||||
</div>
|
||||
<div class="calendar-header">
|
||||
<button class="btn-icon" id="prevMonth">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<h3 id="currentMonth">Loading...</h3>
|
||||
<h3 id="currentMonth">Caricamento...</h3>
|
||||
<button class="btn-icon" id="nextMonth">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button class="btn btn-dark btn-sm" id="quickEntryBtn" style="font-size: 0.85rem;">
|
||||
Inserimento Veloce
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-grid" id="calendarGrid"></div>
|
||||
@@ -66,121 +79,211 @@
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-present"></div>
|
||||
<span>Present (Office)</span>
|
||||
<span>In sede</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-remote"></div>
|
||||
<span>Remote</span>
|
||||
<span>Remoto</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-absent"></div>
|
||||
<span>Absent</span>
|
||||
<span>Assente</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parking Status Section -->
|
||||
<div class="card" id="parkingStatusCard" style="margin-top: 2rem;">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h3>Stato Parcheggio</h3>
|
||||
</div>
|
||||
|
||||
<!-- Daily View Controls -->
|
||||
<div id="dailyViewControls">
|
||||
|
||||
<!-- Date Navigation (Centered) -->
|
||||
<div style="display: flex; justify-content: center; margin-bottom: 2rem;">
|
||||
<div
|
||||
style="display: flex; align-items: center; gap: 0.5rem; background: white; padding: 0.5rem; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<button class="btn-icon" id="statusPrevDay"
|
||||
style="border: none; width: 32px; height: 32px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div style="position: relative; text-align: center; min-width: 200px;">
|
||||
<div id="statusDateDisplay"
|
||||
style="font-weight: 600; font-size: 1rem; text-transform: capitalize;"></div>
|
||||
<input type="date" id="statusDatePicker"
|
||||
style="position: absolute; inset: 0; opacity: 0; cursor: pointer;">
|
||||
</div>
|
||||
|
||||
<button class="btn-icon" id="statusNextDay"
|
||||
style="border: none; width: 32px; height: 32px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Office Header (No Logo) -->
|
||||
<!-- Office Header (No Logo) -->
|
||||
<div
|
||||
style="display: flex; align-items: center; justify-content: flex-start; gap: 1rem; margin-bottom: 1.5rem;">
|
||||
<div style="font-weight: 600; font-size: 1.1rem; color: var(--text);">
|
||||
Ufficio: <span id="currentOfficeDisplay" style="color: var(--primary);">...</span>
|
||||
</div>
|
||||
<span class="badge"
|
||||
style="background: #eff6ff; color: #1d4ed8; border: 1px solid #dbeafe; padding: 0.35rem 0.75rem; font-size: 0.85rem;">
|
||||
Liberi: <span id="spotsCountBadge">0/0</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Spots Grid -->
|
||||
<div id="spotsGrid" style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0;">
|
||||
<!-- Spots injected here -->
|
||||
<div style="width: 100%; text-align: center; color: var(--text-secondary); padding: 2rem;">
|
||||
Caricamento posti...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card parking-map-card" style="margin-top: 2rem;">
|
||||
<h3>Mappa Parcheggio</h3>
|
||||
<img src="/assets/parking-map.png" alt="Mappa Parcheggio"
|
||||
style="width: 100%; height: auto; border-radius: 4px; border: 1px solid var(--border);">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Day Modal -->
|
||||
<div class="modal" id="dayModal" style="display: none;">
|
||||
<!-- Quick Entry Modal -->
|
||||
<div class="modal" id="quickEntryModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3 id="dayModalTitle">Mark Presence</h3>
|
||||
<button class="modal-close" id="closeDayModal">×</button>
|
||||
<h3>Inserimento Veloce</h3>
|
||||
<button class="modal-close" id="closeQuickEntryModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="status-buttons">
|
||||
<button class="status-btn" data-status="present">
|
||||
<div class="status-icon status-present"></div>
|
||||
<span>Present</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="remote">
|
||||
<div class="status-icon status-remote"></div>
|
||||
<span>Remote</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Absent</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="parkingSection" style="display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">
|
||||
<div id="parkingInfo" style="margin-bottom: 0.75rem; font-size: 0.9rem;"></div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary" style="flex: 1;" id="reassignParkingBtn">Reassign</button>
|
||||
<button class="btn btn-secondary" style="flex: 1;" id="releaseParkingBtn">Release</button>
|
||||
<form id="quickEntryForm">
|
||||
<div class="form-group">
|
||||
<label>Range di Date</label>
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<div style="flex: 1;">
|
||||
<small>Da:</small>
|
||||
<input type="date" id="qeStartDate" class="form-control" required>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<small>A (incluso):</small>
|
||||
<input type="date" id="qeEndDate" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-full" id="clearDayBtn" style="margin-top: 1rem;">Clear Presence</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reassign Parking Modal -->
|
||||
<div class="modal" id="reassignModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Reassign Parking Spot</h3>
|
||||
<button class="modal-close" id="closeReassignModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="reassignSpotInfo" style="margin-bottom: 1rem; font-size: 0.9rem;"></p>
|
||||
<div class="form-group">
|
||||
<label for="reassignUser">Assign to</label>
|
||||
<select id="reassignUser" required>
|
||||
<option value="">Select user...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelReassign">Cancel</button>
|
||||
<button type="button" class="btn btn-dark" id="confirmReassign">Reassign</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Stato da applicare</label>
|
||||
<div class="status-buttons">
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="present">
|
||||
<div class="status-icon status-present"></div>
|
||||
<span>In sede</span>
|
||||
</button>
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="remote">
|
||||
<div class="status-icon status-remote"></div>
|
||||
<span>Remoto</span>
|
||||
</button>
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Assente</span>
|
||||
</button>
|
||||
<button type="button" class="status-btn qe-status-btn" data-status="clear">
|
||||
<div class="status-icon"
|
||||
style="border: 2px solid #ef4444; background: #fee2e2; display: flex; align-items: center; justify-content: center;">
|
||||
<span style="color: #ef4444; font-weight: bold; font-size: 1.2rem;">×</span>
|
||||
</div>
|
||||
<span>Rimuovi</span>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="qeStatus" required>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Mark Modal -->
|
||||
<div class="modal" id="bulkMarkModal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Bulk Mark Presence</h3>
|
||||
<button class="modal-close" id="closeBulkModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="bulkMarkForm">
|
||||
<div class="form-group">
|
||||
<label for="startDate">Start Date</label>
|
||||
<input type="date" id="startDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="endDate">End Date</label>
|
||||
<input type="date" id="endDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="bulkStatus">Status</label>
|
||||
<select id="bulkStatus" required>
|
||||
<option value="present">Present (Office)</option>
|
||||
<option value="remote">Remote</option>
|
||||
<option value="absent">Absent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="weekdaysOnly">
|
||||
<span>Weekdays only (Mon-Fri)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelBulk">Cancel</button>
|
||||
<button type="submit" class="btn btn-dark">Mark Dates</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelQuickEntry">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark">Applica</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Day Modal -->
|
||||
<div class="modal" id="dayModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3 id="dayModalTitle">Segna presenza</h3>
|
||||
<button class="modal-close" id="closeDayModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="dayModalUser" style="display:none; margin-bottom: 1rem; font-weight: 500;"></p>
|
||||
<div class="status-buttons">
|
||||
<button class="status-btn" data-status="present">
|
||||
<div class="status-icon status-present"></div>
|
||||
<span>In sede</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="remote">
|
||||
<div class="status-icon status-remote"></div>
|
||||
<span>Remoto</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Assente</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary btn-full" id="clearDayBtn" style="margin-top: 1rem;">Cancella
|
||||
presenza</button>
|
||||
|
||||
<div id="parkingSection"
|
||||
style="display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">
|
||||
<div id="parkingInfo" style="margin-bottom: 0.75rem; font-size: 0.9rem;"></div>
|
||||
|
||||
<div id="parkingActions" style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary" style="flex: 1;" id="reassignParkingBtn">Cedi posto</button>
|
||||
<button class="btn btn-secondary" style="flex: 1;" id="releaseParkingBtn">Lascia libero</button>
|
||||
</div>
|
||||
|
||||
<div id="reassignForm"
|
||||
style="display: none; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<div class="form-group" style="margin-bottom: 0.5rem;">
|
||||
<label for="reassignUser" style="font-size: 0.9rem;">Assegna a</label>
|
||||
<select id="reassignUser" class="form-control" style="width: 100%;">
|
||||
<option value="">Seleziona utente...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button type="button" class="btn btn-secondary" style="flex: 1;"
|
||||
id="cancelReassign">Annulla</button>
|
||||
<button type="button" class="btn btn-dark" style="flex: 1;"
|
||||
id="confirmReassign">Riassegna</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/utils.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/modal-logic.js"></script>
|
||||
<script src="/js/presence.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,31 +8,33 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Loading...</div>
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profile</a>
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Logout</button>
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,43 +42,44 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Profile</h2>
|
||||
<h2>Profilo</h2>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Personal Information</h3>
|
||||
<h3>Informazioni Personali</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- LDAP Notice -->
|
||||
<div id="ldapNotice" class="form-notice" style="display: none; margin-bottom: 1rem;">
|
||||
<small>Your account is managed by LDAP. Some information cannot be changed here.</small>
|
||||
<small>Il tuo account è gestito da LDAP. Alcune informazioni non possono essere modificate
|
||||
qui.</small>
|
||||
</div>
|
||||
|
||||
<form id="profileForm">
|
||||
<div class="form-group">
|
||||
<label for="name">Full Name</label>
|
||||
<label for="name">Nome Completo</label>
|
||||
<input type="text" id="name" required>
|
||||
<small id="nameHelp" class="text-muted" style="display: none;">Managed by LDAP</small>
|
||||
<small id="nameHelp" class="text-muted" style="display: none;">Gestito da LDAP</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" disabled>
|
||||
<small class="text-muted">Email cannot be changed</small>
|
||||
<small class="text-muted">L'email non può essere modificata</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="role">Role</label>
|
||||
<label for="role">Ruolo</label>
|
||||
<input type="text" id="role" disabled>
|
||||
<small class="text-muted">Role is assigned by your administrator</small>
|
||||
<small class="text-muted">Il ruolo è assegnato dal tuo amministratore</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manager">Manager</label>
|
||||
<input type="text" id="manager" disabled>
|
||||
<small class="text-muted">Your manager is assigned by the administrator</small>
|
||||
<small class="text-muted">Il tuo manager è assegnato dall'amministratore</small>
|
||||
</div>
|
||||
<div class="form-actions" id="profileActions">
|
||||
<button type="submit" class="btn btn-dark">Save Changes</button>
|
||||
<button type="submit" class="btn btn-dark">Salva Modifiche</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -84,25 +88,25 @@
|
||||
<!-- Password section - hidden for LDAP users -->
|
||||
<div class="card" id="passwordCard">
|
||||
<div class="card-header">
|
||||
<h3>Change Password</h3>
|
||||
<h3>Cambia Password</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword">Current Password</label>
|
||||
<label for="currentPassword">Password Attuale</label>
|
||||
<input type="password" id="currentPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">New Password</label>
|
||||
<label for="newPassword">Nuova Password</label>
|
||||
<input type="password" id="newPassword" required minlength="8">
|
||||
<small>Minimum 8 characters</small>
|
||||
<small>Minimo 8 caratteri</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm New Password</label>
|
||||
<label for="confirmPassword">Conferma Nuova Password</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-dark">Change Password</button>
|
||||
<button type="submit" class="btn btn-dark">Cambia Password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -135,7 +139,7 @@
|
||||
document.getElementById('name').value = profile.name || '';
|
||||
document.getElementById('email').value = profile.email;
|
||||
document.getElementById('role').value = profile.role;
|
||||
document.getElementById('manager').value = profile.manager_name || 'None';
|
||||
document.getElementById('manager').value = profile.manager_name || 'Nessuno';
|
||||
|
||||
// LDAP mode adjustments
|
||||
if (isLdapUser) {
|
||||
@@ -154,7 +158,7 @@
|
||||
e.preventDefault();
|
||||
|
||||
if (isLdapUser) {
|
||||
utils.showMessage('Profile is managed by LDAP', 'error');
|
||||
utils.showMessage('Il profilo è gestito da LDAP', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -164,13 +168,13 @@
|
||||
|
||||
const response = await api.put('/api/users/me/profile', data);
|
||||
if (response && response.ok) {
|
||||
utils.showMessage('Profile updated successfully', 'success');
|
||||
utils.showMessage('Profilo aggiornato con successo', 'success');
|
||||
// Update nav display
|
||||
const nameEl = document.getElementById('userName');
|
||||
if (nameEl) nameEl.textContent = data.name;
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Failed to update profile', 'error');
|
||||
utils.showMessage(error.detail || 'Impossibile aggiornare il profilo', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,7 +186,7 @@
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
utils.showMessage('Passwords do not match', 'error');
|
||||
utils.showMessage('Le password non corrispondono', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -193,14 +197,15 @@
|
||||
|
||||
const response = await api.post('/api/users/me/change-password', data);
|
||||
if (response && response.ok) {
|
||||
utils.showMessage('Password changed successfully', 'success');
|
||||
utils.showMessage('Password cambiata con successo', 'success');
|
||||
document.getElementById('passwordForm').reset();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Failed to change password', 'error');
|
||||
utils.showMessage(error.detail || 'Impossibile cambiare la password', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,19 +8,20 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-header">
|
||||
<h1>Create Account</h1>
|
||||
<p>Sign up for a new account</p>
|
||||
<h1>Crea Account</h1>
|
||||
<p>Registrati per un nuovo account</p>
|
||||
</div>
|
||||
|
||||
<div id="errorMessage"></div>
|
||||
|
||||
<form id="registerForm">
|
||||
<div class="form-group">
|
||||
<label for="name">Full Name</label>
|
||||
<label for="name">Nome Completo</label>
|
||||
<input type="text" id="name" required autocomplete="name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -29,13 +31,13 @@
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" required autocomplete="new-password" minlength="8">
|
||||
<small>Minimum 8 characters</small>
|
||||
<small>Minimo 8 caratteri</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark btn-full">Create Account</button>
|
||||
<button type="submit" class="btn btn-dark btn-full">Crea Account</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
Already have an account? <a href="/login">Sign in</a>
|
||||
Hai già un account? <a href="/login">Accedi</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,4 +87,5 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,31 +8,33 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Loading...</div>
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profile</a>
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Logout</button>
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,58 +42,41 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Settings</h2>
|
||||
<h2>Impostazioni</h2>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Preferences</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="settingsForm">
|
||||
<div class="form-group">
|
||||
<label for="weekStartDay">Week Starts On</label>
|
||||
<select id="weekStartDay">
|
||||
<option value="0">Sunday</option>
|
||||
<option value="1">Monday</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-dark">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Parking Notifications</h3>
|
||||
<h3>Notifiche Parcheggio</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="notificationForm">
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<span>Weekly Summary</span>
|
||||
<span>Riepilogo Settimanale</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="notifyWeeklyParking">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<small class="text-muted">Receive weekly parking assignments summary every Friday at 12:00</small>
|
||||
<small class="text-muted">Ricevi il riepilogo settimanale delle assegnazioni parcheggio ogni
|
||||
Venerdì alle 12:00</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<span>Daily Reminder</span>
|
||||
<span>Promemoria Giornaliero</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="notifyDailyParking">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<small class="text-muted">Receive daily parking reminder on working days</small>
|
||||
<small class="text-muted">Ricevi promemoria giornaliero nei giorni lavorativi</small>
|
||||
</div>
|
||||
<div class="form-group" id="dailyTimeGroup" style="margin-left: 1rem;">
|
||||
<label>Reminder Time</label>
|
||||
<label>Orario Promemoria</label>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<select id="notifyDailyHour" style="width: 80px;">
|
||||
<!-- Hours populated by JS -->
|
||||
@@ -106,16 +92,17 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<span>Assignment Changes</span>
|
||||
<span>Cambiamenti Assegnazione</span>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="notifyParkingChanges">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</label>
|
||||
<small class="text-muted">Receive immediate notifications when your parking assignment changes</small>
|
||||
<small class="text-muted">Ricevi notifiche immediate quando la tua assegnazione del
|
||||
parcheggio cambia</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-dark">Save Notifications</button>
|
||||
<button type="submit" class="btn btn-dark">Salva Notifiche</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -151,7 +138,7 @@
|
||||
}
|
||||
|
||||
function populateForm() {
|
||||
document.getElementById('weekStartDay').value = currentUser.week_start_day || 0;
|
||||
// Notification settings
|
||||
|
||||
// Notification settings
|
||||
document.getElementById('notifyWeeklyParking').checked = currentUser.notify_weekly_parking !== 0;
|
||||
@@ -170,22 +157,7 @@
|
||||
|
||||
function setupEventListeners() {
|
||||
// Settings form
|
||||
document.getElementById('settingsForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
week_start_day: parseInt(document.getElementById('weekStartDay').value)
|
||||
};
|
||||
|
||||
const response = await api.put('/api/users/me/settings', data);
|
||||
if (response && response.ok) {
|
||||
utils.showMessage('Settings saved successfully', 'success');
|
||||
currentUser = await api.getCurrentUser();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Failed to save settings', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Notification form
|
||||
document.getElementById('notificationForm').addEventListener('submit', async (e) => {
|
||||
@@ -201,11 +173,11 @@
|
||||
|
||||
const response = await api.put('/api/users/me/settings', data);
|
||||
if (response && response.ok) {
|
||||
utils.showMessage('Notification settings saved', 'success');
|
||||
utils.showMessage('Impostazioni notifiche salvate', 'success');
|
||||
currentUser = await api.getCurrentUser();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
utils.showMessage(error.detail || 'Failed to save notifications', 'error');
|
||||
utils.showMessage(error.detail || 'Impossibile salvare le notifiche', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -214,4 +186,5 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -7,31 +8,33 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Loading...</div>
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profile</a>
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Logout</button>
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,36 +42,46 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Team Calendar</h2>
|
||||
<h2>Calendario del Team</h2>
|
||||
<div class="header-actions">
|
||||
<select id="viewToggle" class="form-select" style="min-width: 100px;">
|
||||
<option value="week">Week</option>
|
||||
<option value="month">Month</option>
|
||||
</select>
|
||||
<select id="managerFilter" class="form-select">
|
||||
<option value="">All Managers</option>
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper">
|
||||
<div class="card">
|
||||
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem;">
|
||||
<select id="viewToggle" class="form-select" style="min-width: 150px;">
|
||||
<option value="week">Settimana</option>
|
||||
<option value="month">Mese</option>
|
||||
</select>
|
||||
<select id="officeFilter" class="form-select" style="min-width: 200px;">
|
||||
<option value="">Tutti gli Uffici</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="office-display-header"
|
||||
style="margin-bottom: 1rem; font-weight: 600; font-size: 1.1rem; color: var(--text);">
|
||||
Ufficio: <span id="currentOfficeNameDisplay" style="color: var(--primary);">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div class="calendar-header">
|
||||
<button class="btn-icon" id="prevWeek">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<h3 id="currentWeek">Loading...</h3>
|
||||
<h3 id="currentWeek">Caricamento...</h3>
|
||||
<button class="btn-icon" id="nextWeek">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="team-calendar-container">
|
||||
<table class="team-calendar-table" id="teamCalendarTable">
|
||||
<table class="team-calendar team-calendar-table" id="teamCalendarTable">
|
||||
<thead>
|
||||
<tr id="calendarHeader"></tr>
|
||||
</thead>
|
||||
@@ -79,71 +92,71 @@
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-present"></div>
|
||||
<span>Present</span>
|
||||
<span>In sede</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-remote"></div>
|
||||
<span>Remote</span>
|
||||
<span>Remoto</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color status-absent"></div>
|
||||
<span>Absent</span>
|
||||
<span>Assente</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Day Status Modal -->
|
||||
<!-- Day Status Modal (Shared Structure) -->
|
||||
<div class="modal" id="dayModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3 id="dayModalTitle">Mark Presence</h3>
|
||||
<h3 id="dayModalTitle">Segna Presenza</h3>
|
||||
<button class="modal-close" id="closeDayModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="dayModalUser" style="margin-bottom: 1rem; font-weight: 500;"></p>
|
||||
<p id="dayModalUser" style="display:none; margin-bottom: 1rem; font-weight: 500;"></p>
|
||||
<div class="status-buttons">
|
||||
<button class="status-btn" data-status="present">
|
||||
<div class="status-icon status-present"></div>
|
||||
<span>Present</span>
|
||||
<span>In sede</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="remote">
|
||||
<div class="status-icon status-remote"></div>
|
||||
<span>Remote</span>
|
||||
<span>Remoto</span>
|
||||
</button>
|
||||
<button class="status-btn" data-status="absent">
|
||||
<div class="status-icon status-absent"></div>
|
||||
<span>Absent</span>
|
||||
<span>Assente</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="parkingSection" style="display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">
|
||||
<div id="parkingInfo" style="margin-bottom: 0.75rem; font-size: 0.9rem;"></div>
|
||||
<button class="btn btn-secondary btn-full" id="reassignParkingBtn">Reassign Spot</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-full" id="clearDayBtn" style="margin-top: 1rem;">Clear Presence</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-full" id="clearDayBtn" style="margin-top: 1rem;">Cancella
|
||||
Presenza</button>
|
||||
|
||||
<!-- Reassign Parking Modal -->
|
||||
<div class="modal" id="reassignModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Reassign Parking Spot</h3>
|
||||
<button class="modal-close" id="closeReassignModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="reassignSpotInfo" style="margin-bottom: 1rem; font-size: 0.9rem;"></p>
|
||||
<div class="form-group">
|
||||
<label for="reassignUser">Assign to</label>
|
||||
<select id="reassignUser" required>
|
||||
<option value="">Select user...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelReassign">Cancel</button>
|
||||
<button type="button" class="btn btn-dark" id="confirmReassign">Reassign</button>
|
||||
<div id="parkingSection"
|
||||
style="display: none; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">
|
||||
<div id="parkingInfo" style="margin-bottom: 0.75rem; font-size: 0.9rem;"></div>
|
||||
|
||||
<div id="parkingActions" style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary" style="flex: 1;" id="reassignParkingBtn">Cedi posto</button>
|
||||
<button class="btn btn-secondary" style="flex: 1;" id="releaseParkingBtn">Lascia libero</button>
|
||||
</div>
|
||||
|
||||
<div id="reassignForm"
|
||||
style="display: none; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<div class="form-group" style="margin-bottom: 0.5rem;">
|
||||
<label for="reassignUser" style="font-size: 0.9rem;">Assegna a</label>
|
||||
<select id="reassignUser" class="form-control" style="width: 100%;">
|
||||
<option value="">Seleziona utente...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button type="button" class="btn btn-secondary" style="flex: 1;"
|
||||
id="cancelReassign">Annulla</button>
|
||||
<button type="button" class="btn btn-dark" style="flex: 1;"
|
||||
id="confirmReassign">Riassegna</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,6 +165,8 @@
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/utils.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/modal-logic.js"></script>
|
||||
<script src="/js/team-calendar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,37 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Team Rules - Parking Manager</title>
|
||||
<title>Regole Parcheggio - Parking Manager</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Parking Manager</h1>
|
||||
<h1>Gestione Parcheggi</h1>
|
||||
</div>
|
||||
<nav class="sidebar-nav"></nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-menu">
|
||||
<button class="user-button" id="userMenuButton">
|
||||
<div class="user-avatar">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name" id="userName">Loading...</div>
|
||||
<div class="user-name" id="userName">Caricamento...</div>
|
||||
<div class="user-role" id="userRole">-</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="user-dropdown" id="userDropdown" style="display: none;">
|
||||
<a href="/profile" class="dropdown-item">Profile</a>
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<a href="/profile" class="dropdown-item">Profilo</a>
|
||||
<a href="/settings" class="dropdown-item">Impostazioni</a>
|
||||
<hr class="dropdown-divider">
|
||||
<button class="dropdown-item" id="logoutButton">Logout</button>
|
||||
<button class="dropdown-item" id="logoutButton">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,100 +42,120 @@
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
<h2>Team Rules</h2>
|
||||
<h2>Regole Parcheggio</h2>
|
||||
<div class="header-actions">
|
||||
<select id="managerSelect" class="form-select">
|
||||
<option value="">Select Manager</option>
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-wrapper" id="rulesContent" style="display: none;">
|
||||
<!-- Weekly Closing Days -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Weekly Closing Days</h3>
|
||||
<div class="content-wrapper">
|
||||
<!-- Office Selection Card -->
|
||||
<div class="card" id="officeSelectionCard" style="margin-bottom: 1.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<label for="officeSelect" style="font-weight: 500; min-width: max-content;">Seleziona
|
||||
Ufficio:</label>
|
||||
<select id="officeSelect" class="form-select" style="flex: 1; max-width: 300px;">
|
||||
<option value="">Seleziona Ufficio</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">Days of the week parking is unavailable</p>
|
||||
<div class="weekday-checkboxes" id="weeklyClosingDays">
|
||||
<label><input type="checkbox" data-weekday="0"> Sunday</label>
|
||||
<label><input type="checkbox" data-weekday="1"> Monday</label>
|
||||
<label><input type="checkbox" data-weekday="2"> Tuesday</label>
|
||||
<label><input type="checkbox" data-weekday="3"> Wednesday</label>
|
||||
<label><input type="checkbox" data-weekday="4"> Thursday</label>
|
||||
<label><input type="checkbox" data-weekday="5"> Friday</label>
|
||||
<label><input type="checkbox" data-weekday="6"> Saturday</label>
|
||||
</div>
|
||||
|
||||
<div id="rulesContent" style="display: none;">
|
||||
<!-- Weekly Closing Days -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Giorni di Chiusura Settimanale</h3>
|
||||
<button class="btn btn-primary btn-sm" id="saveWeeklyClosingDaysBtn">Salva</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">Giorni della settimana in cui il parcheggio
|
||||
non è
|
||||
disponibile</p>
|
||||
<div class="weekday-checkboxes" id="weeklyClosingDays">
|
||||
<label><input type="checkbox" data-weekday="1"> Lunedì</label>
|
||||
<label><input type="checkbox" data-weekday="2"> Martedì</label>
|
||||
<label><input type="checkbox" data-weekday="3"> Mercoledì</label>
|
||||
<label><input type="checkbox" data-weekday="4"> Giovedì</label>
|
||||
<label><input type="checkbox" data-weekday="5"> Venerdì</label>
|
||||
<label><input type="checkbox" data-weekday="6"> Sabato</label>
|
||||
<label><input type="checkbox" data-weekday="0"> Domenica</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Specific Closing Days -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Specific Closing Days</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="addClosingDayBtn">Add</button>
|
||||
<!-- Specific Closing Days -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Giorni di Chiusura Specifici</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="addClosingDayBtn">Aggiungi</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Date specifiche in cui il parcheggio non è disponibile (festività, ecc.)
|
||||
</p>
|
||||
<div id="closingDaysList" class="rules-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Specific dates when parking is unavailable (holidays, etc.)</p>
|
||||
<div id="closingDaysList" class="rules-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parking Guarantees -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Parking Guarantees</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="addGuaranteeBtn">Add</button>
|
||||
<!-- Parking Guarantees -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Garanzie di Parcheggio</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="addGuaranteeBtn">Aggiungi</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Utenti a cui è garantito un posto auto quando sono presenti</p>
|
||||
<div id="guaranteesList" class="rules-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Users guaranteed a parking spot when present</p>
|
||||
<div id="guaranteesList" class="rules-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parking Exclusions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Parking Exclusions</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="addExclusionBtn">Add</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Users excluded from parking assignment</p>
|
||||
<div id="exclusionsList" class="rules-list"></div>
|
||||
<!-- Parking Exclusions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Esclusioni Parcheggio</h3>
|
||||
<button class="btn btn-secondary btn-sm" id="addExclusionBtn">Aggiungi</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Utenti esclusi dall'assegnazione del parcheggio</p>
|
||||
<div id="exclusionsList" class="rules-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-wrapper" id="noManagerMessage">
|
||||
<div id="noOfficeMessage">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<p>Select a manager to manage their parking rules</p>
|
||||
<p>Seleziona un ufficio sopra per gestirne le regole di parcheggio</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Add Closing Day Modal -->
|
||||
<div class="modal" id="closingDayModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Add Closing Day</h3>
|
||||
<h3>Aggiungi Giorno di Chiusura</h3>
|
||||
<button class="modal-close" id="closeClosingDayModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="closingDayForm">
|
||||
<div class="form-group">
|
||||
<label for="closingDate">Date</label>
|
||||
<label for="closingDate">Data Inizio</label>
|
||||
<input type="date" id="closingDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="closingReason">Reason (optional)</label>
|
||||
<input type="text" id="closingReason" placeholder="e.g., Company holiday">
|
||||
<label for="closingEndDate">Data Fine (opzionale)</label>
|
||||
<input type="date" id="closingEndDate">
|
||||
<small>Lascia vuoto per singolo giorno</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="closingReason">Motivo (opzionale)</label>
|
||||
<input type="text" id="closingReason" placeholder="es. Ferie aziendali">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelClosingDay">Cancel</button>
|
||||
<button type="submit" class="btn btn-dark">Add</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelClosingDay">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark">Aggiungi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -143,30 +166,33 @@
|
||||
<div class="modal" id="guaranteeModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Add Parking Guarantee</h3>
|
||||
<h3>Aggiungi Garanzia Parcheggio</h3>
|
||||
<button class="modal-close" id="closeGuaranteeModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="guaranteeForm">
|
||||
<div class="form-group">
|
||||
<label for="guaranteeUser">User</label>
|
||||
<label for="guaranteeUser">Utente</label>
|
||||
<select id="guaranteeUser" required>
|
||||
<option value="">Select user...</option>
|
||||
<option value="">Seleziona utente...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="guaranteeStartDate">Start Date (optional)</label>
|
||||
<label for="guaranteeStartDate">Data Inizio (opzionale)</label>
|
||||
<input type="date" id="guaranteeStartDate">
|
||||
<small>Leave empty for no start limit</small>
|
||||
<small>Lascia vuoto per nessun limite inziale</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="guaranteeEndDate">End Date (optional)</label>
|
||||
<input type="date" id="guaranteeEndDate">
|
||||
<small>Leave empty for no end limit</small>
|
||||
<small>Lascia vuoto per nessun limite finale</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="guaranteeNotes">Note (opzionale)</label>
|
||||
<textarea id="guaranteeNotes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelGuarantee">Cancel</button>
|
||||
<button type="submit" class="btn btn-dark">Add</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelGuarantee">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark">Aggiungi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -177,30 +203,33 @@
|
||||
<div class="modal" id="exclusionModal" style="display: none;">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Add Parking Exclusion</h3>
|
||||
<h3>Aggiungi Esclusione Parcheggio</h3>
|
||||
<button class="modal-close" id="closeExclusionModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="exclusionForm">
|
||||
<div class="form-group">
|
||||
<label for="exclusionUser">User</label>
|
||||
<label for="exclusionUser">Utente</label>
|
||||
<select id="exclusionUser" required>
|
||||
<option value="">Select user...</option>
|
||||
<option value="">Seleziona utente...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exclusionStartDate">Start Date (optional)</label>
|
||||
<label for="exclusionStartDate">Data Inizio (opzionale)</label>
|
||||
<input type="date" id="exclusionStartDate">
|
||||
<small>Leave empty for no start limit</small>
|
||||
<small>Lascia vuoto per nessun limite iniziale</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exclusionEndDate">End Date (optional)</label>
|
||||
<input type="date" id="exclusionEndDate">
|
||||
<small>Leave empty for no end limit</small>
|
||||
<small>Lascia vuoto per nessun limite finale</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="exclusionNotes">Note (opzionale)</label>
|
||||
<textarea id="exclusionNotes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="cancelExclusion">Cancel</button>
|
||||
<button type="submit" class="btn btn-dark">Add</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancelExclusion">Annulla</button>
|
||||
<button type="submit" class="btn btn-dark">Aggiungi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -212,4 +241,5 @@
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/team-rules.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user