piccoli fix
This commit is contained in:
@@ -26,10 +26,10 @@ router = APIRouter(prefix="/api/offices", tags=["offices"])
|
|||||||
class ValidOfficeCreate(BaseModel):
|
class ValidOfficeCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
parking_quota: int = 0
|
parking_quota: int = 0
|
||||||
booking_window_enabled: bool = True
|
booking_window_enabled: bool = False
|
||||||
booking_window_end_hour: int = 18
|
booking_window_end_hour: int = 18
|
||||||
booking_window_end_minute: int = 0
|
booking_window_end_minute: int = 0
|
||||||
assignment_mode: str = "fairness"
|
assignment_mode: str = "random"
|
||||||
|
|
||||||
|
|
||||||
class ClosingDayCreate(BaseModel):
|
class ClosingDayCreate(BaseModel):
|
||||||
@@ -92,7 +92,11 @@ def list_offices(db: Session = Depends(get_db), user=Depends(require_manager_or_
|
|||||||
"name": office.name,
|
"name": office.name,
|
||||||
"parking_quota": office.parking_quota,
|
"parking_quota": office.parking_quota,
|
||||||
"spot_prefix": office.spot_prefix,
|
"spot_prefix": office.spot_prefix,
|
||||||
"user_count": user_counts.get(office.id, 0)
|
"user_count": user_counts.get(office.id, 0),
|
||||||
|
"booking_window_enabled": office.booking_window_enabled,
|
||||||
|
"booking_window_end_hour": office.booking_window_end_hour,
|
||||||
|
"booking_window_end_minute": office.booking_window_end_minute,
|
||||||
|
"assignment_mode": office.assignment_mode
|
||||||
}
|
}
|
||||||
for office in offices
|
for office in offices
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ let currentUser = null;
|
|||||||
let offices = [];
|
let offices = [];
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
populateTimeSelects();
|
||||||
currentUser = await api.requireAuth();
|
currentUser = await api.requireAuth();
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
await loadOffices();
|
await loadOffices();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
populateTimeSelects();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function populateTimeSelects() {
|
function populateTimeSelects() {
|
||||||
@@ -48,7 +48,7 @@ function populateTimeSelects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadOffices() {
|
async function loadOffices() {
|
||||||
const response = await api.getCached('/api/offices', 60);
|
const response = await api.get('/api/offices'); // Force fresh load
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
offices = await response.json();
|
offices = await response.json();
|
||||||
renderOffices();
|
renderOffices();
|
||||||
@@ -98,14 +98,30 @@ async function editOffice(officeId) {
|
|||||||
document.getElementById('officeName').value = office.name;
|
document.getElementById('officeName').value = office.name;
|
||||||
document.getElementById('officeQuota').value = office.parking_quota;
|
document.getElementById('officeQuota').value = office.parking_quota;
|
||||||
|
|
||||||
// Set booking window settings
|
// Set assignment mode mapping
|
||||||
document.getElementById('officeWindowEnabled').checked = office.booking_window_enabled !== false;
|
const modeSelect = document.getElementById('assignmentModeSelect');
|
||||||
|
if (office.booking_window_enabled === false) {
|
||||||
|
modeSelect.value = 'realtime';
|
||||||
|
} else {
|
||||||
|
modeSelect.value = office.assignment_mode || 'random';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cutoff time
|
||||||
document.getElementById('officeCutoffHour').value = office.booking_window_end_hour != null ? office.booking_window_end_hour : 18;
|
document.getElementById('officeCutoffHour').value = office.booking_window_end_hour != null ? office.booking_window_end_hour : 18;
|
||||||
document.getElementById('officeCutoffMinute').value = office.booking_window_end_minute != null ? office.booking_window_end_minute : 0;
|
document.getElementById('officeCutoffMinute').value = office.booking_window_end_minute != null ? office.booking_window_end_minute : 0;
|
||||||
|
|
||||||
|
updateCutoffVisibility();
|
||||||
openModal('Modifica Gruppo');
|
openModal('Modifica Gruppo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateCutoffVisibility() {
|
||||||
|
const mode = document.getElementById('assignmentModeSelect').value;
|
||||||
|
const group = document.getElementById('cutoffGroup');
|
||||||
|
if (group) {
|
||||||
|
group.style.display = (mode === 'realtime') ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteOffice(officeId) {
|
async function deleteOffice(officeId) {
|
||||||
const office = offices.find(o => o.id === officeId);
|
const office = offices.find(o => o.id === officeId);
|
||||||
if (!office) return;
|
if (!office) return;
|
||||||
@@ -143,6 +159,9 @@ function setupEventListeners() {
|
|||||||
// Form submit
|
// Form submit
|
||||||
const form = document.getElementById('officeForm');
|
const form = document.getElementById('officeForm');
|
||||||
form.addEventListener('submit', handleOfficeSubmit);
|
form.addEventListener('submit', handleOfficeSubmit);
|
||||||
|
|
||||||
|
// Assignment mode change
|
||||||
|
document.getElementById('assignmentModeSelect').addEventListener('change', updateCutoffVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOfficeSubmit(e) {
|
async function handleOfficeSubmit(e) {
|
||||||
@@ -155,12 +174,15 @@ async function handleOfficeSubmit(e) {
|
|||||||
saveBtn.innerHTML = 'Salvataggio...';
|
saveBtn.innerHTML = 'Salvataggio...';
|
||||||
|
|
||||||
const officeId = document.getElementById('officeId').value;
|
const officeId = document.getElementById('officeId').value;
|
||||||
|
const mode = document.getElementById('assignmentModeSelect').value;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name: document.getElementById('officeName').value,
|
name: document.getElementById('officeName').value,
|
||||||
parking_quota: parseInt(document.getElementById('officeQuota').value) || 0,
|
parking_quota: parseInt(document.getElementById('officeQuota').value) || 0,
|
||||||
booking_window_enabled: document.getElementById('officeWindowEnabled').checked,
|
booking_window_enabled: mode !== 'realtime',
|
||||||
booking_window_end_hour: parseInt(document.getElementById('officeCutoffHour').value),
|
booking_window_end_hour: parseInt(document.getElementById('officeCutoffHour').value),
|
||||||
booking_window_end_minute: parseInt(document.getElementById('officeCutoffMinute').value)
|
booking_window_end_minute: parseInt(document.getElementById('officeCutoffMinute').value),
|
||||||
|
assignment_mode: mode === 'realtime' ? 'random' : mode
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Payload:', data);
|
console.log('Payload:', data);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const ModalLogic = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openModal(data) {
|
openModal(data) {
|
||||||
const { dateStr, userName, presence, parking, userId, isReadOnly } = data;
|
const { dateStr, userName, presence, parking, userId, isParkingOnly } = data;
|
||||||
|
|
||||||
this.currentDate = dateStr;
|
this.currentDate = dateStr;
|
||||||
this.currentUserId = userId; // Optional, for team view
|
this.currentUserId = userId; // Optional, for team view
|
||||||
@@ -84,8 +84,9 @@ const ModalLogic = {
|
|||||||
const modal = document.getElementById('dayModal');
|
const modal = document.getElementById('dayModal');
|
||||||
const title = document.getElementById('dayModalTitle');
|
const title = document.getElementById('dayModalTitle');
|
||||||
const userLabel = document.getElementById('dayModalUser');
|
const userLabel = document.getElementById('dayModalUser');
|
||||||
|
const statusButtons = document.querySelector('#dayModal .status-buttons');
|
||||||
|
const clearBtn = document.getElementById('clearDayBtn');
|
||||||
|
|
||||||
title.textContent = utils.formatDateDisplay(dateStr);
|
|
||||||
|
|
||||||
// Show/Hide User Name (for Team Calendar)
|
// Show/Hide User Name (for Team Calendar)
|
||||||
if (userName && userLabel) {
|
if (userName && userLabel) {
|
||||||
@@ -95,22 +96,26 @@ const ModalLogic = {
|
|||||||
userLabel.style.display = 'none';
|
userLabel.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight status
|
// Presence Logic
|
||||||
document.querySelectorAll('#dayModal .status-btn').forEach(btn => {
|
if (isParkingOnly) {
|
||||||
const status = btn.dataset.status;
|
if (statusButtons) statusButtons.style.display = 'none';
|
||||||
if (presence && presence.status === status) {
|
if (clearBtn) clearBtn.style.display = 'none';
|
||||||
btn.classList.add('active');
|
title.textContent = `Gestione Parcheggio - ${utils.formatDateDisplay(dateStr)}`;
|
||||||
} else {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear button visibility
|
|
||||||
const clearBtn = document.getElementById('clearDayBtn');
|
|
||||||
if (presence) {
|
|
||||||
clearBtn.style.display = 'block';
|
|
||||||
} else {
|
} else {
|
||||||
clearBtn.style.display = 'none';
|
if (statusButtons) statusButtons.style.display = 'grid';
|
||||||
|
if (clearBtn) clearBtn.style.display = presence ? 'block' : 'none';
|
||||||
|
|
||||||
|
title.textContent = utils.formatDateDisplay(dateStr);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parking Section & Reset Form
|
// Parking Section & Reset Form
|
||||||
@@ -122,7 +127,8 @@ const ModalLogic = {
|
|||||||
if (parking) {
|
if (parking) {
|
||||||
parkingSection.style.display = 'block';
|
parkingSection.style.display = 'block';
|
||||||
const spotName = parking.spot_display_name || parking.spot_id;
|
const spotName = parking.spot_display_name || parking.spot_id;
|
||||||
parkingInfo.innerHTML = `<strong>Parcheggio:</strong> Posto ${spotName}`;
|
const occupantInfo = userName ? ` (Occupato da ${userName})` : '';
|
||||||
|
parkingInfo.innerHTML = `<strong>Posto ${spotName}</strong>${occupantInfo}`;
|
||||||
} else {
|
} else {
|
||||||
parkingSection.style.display = 'none';
|
parkingSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ const NAV_ITEMS = [
|
|||||||
{ href: '/team-calendar', icon: 'users', label: 'Calendario del team' },
|
{ href: '/team-calendar', icon: 'users', label: 'Calendario del team' },
|
||||||
{ href: '/team-rules', icon: 'rules', label: 'Regole parcheggio', roles: ['admin', 'manager'] },
|
{ href: '/team-rules', icon: 'rules', label: 'Regole parcheggio', roles: ['admin', 'manager'] },
|
||||||
{ href: '/admin/users', icon: 'user', label: 'Gestione Utenti', roles: ['admin'] },
|
{ href: '/admin/users', icon: 'user', label: 'Gestione Utenti', roles: ['admin'] },
|
||||||
{ href: '/admin/offices', icon: 'building', label: 'Gestione Gruppi', roles: ['admin'] },
|
{ href: '/admin/offices', icon: 'building', label: 'Gestione Gruppi', roles: ['admin'] }
|
||||||
{ href: '/parking-settings', icon: 'settings', label: 'Impostazioni Gruppi', roles: ['admin', 'manager'] }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function getIcon(name) {
|
function getIcon(name) {
|
||||||
|
|||||||
@@ -1,373 +0,0 @@
|
|||||||
|
|
||||||
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 gruppi', '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 gruppo 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const minuteSelect = document.getElementById('bookingWindowMinute');
|
|
||||||
minuteSelect.innerHTML = '';
|
|
||||||
for (let m = 0; m < 60; m++) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = m;
|
|
||||||
option.textContent = m.toString().padStart(2, '0');
|
|
||||||
minuteSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOfficeSettings(id) {
|
|
||||||
const officeId = id;
|
|
||||||
if (!officeId) {
|
|
||||||
utils.showMessage('Nessun gruppo 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('assignmentModeSelect').value = office.assignment_mode || 'fairness';
|
|
||||||
document.getElementById('bookingWindowEnabled').checked = office.booking_window_enabled || false;
|
|
||||||
document.getElementById('bookingWindowHour').value = office.booking_window_end_hour ?? 18; // Default 18
|
|
||||||
document.getElementById('bookingWindowMinute').value = office.booking_window_end_minute ?? 0;
|
|
||||||
|
|
||||||
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 = {
|
|
||||||
assignment_mode: document.getElementById('assignmentModeSelect').value,
|
|
||||||
booking_window_enabled: document.getElementById('bookingWindowEnabled').checked,
|
|
||||||
booking_window_end_hour: parseInt(document.getElementById('bookingWindowHour').value),
|
|
||||||
booking_window_end_minute: parseInt(document.getElementById('bookingWindowMinute').value)
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
// 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 = utils.formatDate(current);
|
|
||||||
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');
|
|
||||||
|
|
||||||
// Loop is fine, but maybe redundant if we could batch clean?
|
|
||||||
// Backend clear-assignments is per day.
|
|
||||||
while (current <= end) {
|
|
||||||
const dateStr = utils.formatDate(current);
|
|
||||||
try {
|
|
||||||
const res = await api.post('/api/parking/clear-assignments', {
|
|
||||||
date: dateStr,
|
|
||||||
office_id: currentOffice.id
|
|
||||||
});
|
|
||||||
if (res && res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
totalRemoved += (data.count || 0);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error clearing ${dateStr}`, e);
|
|
||||||
}
|
|
||||||
current.setDate(current.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.showMessage(`Operazione eseguita.`, 'warning');
|
|
||||||
});
|
|
||||||
|
|
||||||
const clearPresenceBtn = document.getElementById('clearPresenceBtn');
|
|
||||||
if (clearPresenceBtn) {
|
|
||||||
clearPresenceBtn.addEventListener('click', async () => {
|
|
||||||
if (!confirm('ATTENZIONE: Stai per eliminare TUTTI GLI STATI (Presente/Assente/ecc) e relative assegnazioni per tutti gli utenti del gruppo nel periodo selezionato. \n\nQuesta azione è irreversibile. 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');
|
|
||||||
|
|
||||||
// Validate office
|
|
||||||
if (!currentOffice || !currentOffice.id) {
|
|
||||||
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
const endDateVal = dateEnd || dateStart;
|
|
||||||
|
|
||||||
utils.showMessage('Rimozione stati in corso...', 'warning');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await api.post('/api/presence/admin/clear-office-presence', {
|
|
||||||
start_date: dateStart,
|
|
||||||
end_date: endDateVal,
|
|
||||||
office_id: currentOffice.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
utils.showMessage(`Operazione completata. Rimossi ${data.count_presence} stati e ${data.count_parking} assegnazioni.`, 'success');
|
|
||||||
} else {
|
|
||||||
const err = await res.json();
|
|
||||||
utils.showMessage('Errore: ' + (err.detail || 'Operazione fallita'), 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
utils.showMessage('Errore di comunicazione col server', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const testEmailBtn = document.getElementById('testEmailBtn');
|
|
||||||
if (testEmailBtn) {
|
|
||||||
testEmailBtn.addEventListener('click', async () => {
|
|
||||||
const dateVal = document.getElementById('testEmailDate').value;
|
|
||||||
|
|
||||||
// Validate office
|
|
||||||
if (!currentOffice || !currentOffice.id) {
|
|
||||||
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.showMessage('Invio mail di test in corso...', 'warning');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await api.post('/api/parking/test-email', {
|
|
||||||
date: dateVal || null,
|
|
||||||
office_id: currentOffice.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status >= 200 && res.status < 300) {
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
let msg = `Email inviata con successo per la data: ${data.date}.`;
|
|
||||||
if (data.mode === 'FILE') {
|
|
||||||
msg += ' (SMTP disabilitato: Loggato su file)';
|
|
||||||
}
|
|
||||||
utils.showMessage(msg, 'success');
|
|
||||||
} else {
|
|
||||||
utils.showMessage('Invio fallito. Controlla i log del server.', 'error');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const err = res ? await res.json() : {};
|
|
||||||
console.error("Test Email Error:", err);
|
|
||||||
const errMsg = err.detail ? (typeof err.detail === 'object' ? JSON.stringify(err.detail) : err.detail) : 'Invio fallito';
|
|
||||||
utils.showMessage('Errore: ' + errMsg, 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
utils.showMessage('Errore di comunicazione col server', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const bulkEmailBtn = document.getElementById('bulkEmailBtn');
|
|
||||||
if (bulkEmailBtn) {
|
|
||||||
bulkEmailBtn.addEventListener('click', async () => {
|
|
||||||
const dateVal = document.getElementById('testEmailDate').value;
|
|
||||||
|
|
||||||
// Validate office
|
|
||||||
if (!currentOffice || !currentOffice.id) {
|
|
||||||
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dateVal) {
|
|
||||||
return utils.showMessage('Per il test a TUTTI è obbligatorio selezionare una data specifica.', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm(`Sei sicuro di voler inviare una mail di promemoria a TUTTI gli utenti con parcheggio assegnato per il giorno ${dateVal}?\n\nQuesta azione invierà vere email.`)) return;
|
|
||||||
|
|
||||||
utils.showMessage('Invio mail massive in corso...', 'warning');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await api.post('/api/parking/test-email', {
|
|
||||||
date: dateVal,
|
|
||||||
office_id: currentOffice.id,
|
|
||||||
bulk_send: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status >= 200 && res.status < 300) {
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.success) {
|
|
||||||
let msg = `Processo completato per il ${data.date}. Inviate: ${data.count || 0}, Fallite: ${data.failed || 0}.`;
|
|
||||||
if (data.mode === 'BULK' && (data.count || 0) === 0) msg += " (Nessun assegnatario trovato)";
|
|
||||||
utils.showMessage(msg, 'success');
|
|
||||||
} else {
|
|
||||||
utils.showMessage('Errore durante l\'invio.', 'error');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const err = res ? await res.json() : {};
|
|
||||||
console.error("Bulk Test Email Error:", err);
|
|
||||||
const errMsg = err.detail ? (typeof err.detail === 'object' ? JSON.stringify(err.detail) : err.detail) : 'Invio fallito';
|
|
||||||
utils.showMessage('Errore: ' + errMsg, 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
utils.showMessage('Errore di comunicazione col server: ' + e.message, 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -234,36 +234,44 @@ async function handleClearPresence(date) {
|
|||||||
async function handleReleaseParking(assignmentId) {
|
async function handleReleaseParking(assignmentId) {
|
||||||
if (!confirm('Rilasciare il parcheggio per questa data?')) return;
|
if (!confirm('Rilasciare il parcheggio per questa data?')) return;
|
||||||
|
|
||||||
const response = await api.post(`/api/parking/release-my-spot/${assignmentId}`);
|
utils.showMessage('Rilascio in corso...', 'warning');
|
||||||
|
const response = await api.post('/api/parking/reassign-spot', {
|
||||||
|
assignment_id: assignmentId,
|
||||||
|
new_user_id: null
|
||||||
|
});
|
||||||
|
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
await loadParkingAssignments();
|
utils.showMessage('Posto liberato con successo', 'success');
|
||||||
|
await Promise.all([loadParkingAssignments(), loadDailyStatus()]);
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
ModalLogic.closeModal();
|
ModalLogic.closeModal();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert(error.detail || 'Impossibile rilasciare il parcheggio');
|
utils.showMessage(error.detail || 'Impossibile rilasciare il parcheggio', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleReassignParking(assignmentId, newUserId) {
|
async function handleReassignParking(assignmentId, newUserId) {
|
||||||
// Basic validation handled by select; confirm
|
// Basic validation handled by select; confirm
|
||||||
if (!assignmentId || !newUserId) {
|
if (!assignmentId || !newUserId) {
|
||||||
alert('Seleziona un utente');
|
utils.showMessage('Seleziona un utente', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
utils.showMessage('Riassegnazione in corso...', 'warning');
|
||||||
const response = await api.post('/api/parking/reassign-spot', {
|
const response = await api.post('/api/parking/reassign-spot', {
|
||||||
assignment_id: assignmentId,
|
assignment_id: assignmentId,
|
||||||
new_user_id: newUserId
|
new_user_id: newUserId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && response.ok) {
|
if (response && response.ok) {
|
||||||
await loadParkingAssignments();
|
utils.showMessage('Posto riassegnato con successo', 'success');
|
||||||
|
await Promise.all([loadParkingAssignments(), loadDailyStatus()]);
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
ModalLogic.closeModal();
|
ModalLogic.closeModal();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert(error.detail || 'Impossibile riassegnare il parcheggio');
|
utils.showMessage(error.detail || 'Impossibile riassegnare il parcheggio', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,25 +584,14 @@ function renderParkingStatus(assignments) {
|
|||||||
el.style.cursor = 'pointer';
|
el.style.cursor = 'pointer';
|
||||||
el.addEventListener('mouseenter', () => el.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)');
|
el.addEventListener('mouseenter', () => el.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)');
|
||||||
el.addEventListener('mouseleave', () => el.style.boxShadow = 'none');
|
el.addEventListener('mouseleave', () => el.style.boxShadow = 'none');
|
||||||
el.title = "Clicca per liberare questo posto";
|
el.title = "Clicca per gestire questo posto";
|
||||||
el.addEventListener('click', async () => {
|
el.addEventListener('click', () => {
|
||||||
if (!confirm(`Vuoi liberare il posto ${spotName} occupato da ${statusText}?`)) return;
|
ModalLogic.openModal({
|
||||||
|
dateStr: a.date,
|
||||||
utils.showMessage('Rilascio in corso...', 'warning');
|
parking: a,
|
||||||
const response = await api.post('/api/parking/reassign-spot', {
|
userName: a.user_name,
|
||||||
assignment_id: a.id,
|
isParkingOnly: true
|
||||||
new_user_id: null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && response.ok) {
|
|
||||||
utils.showMessage('Posto liberato con successo', 'success');
|
|
||||||
loadDailyStatus();
|
|
||||||
loadParkingAssignments();
|
|
||||||
renderCalendar();
|
|
||||||
} else {
|
|
||||||
const err = await response.json();
|
|
||||||
utils.showMessage(err.detail || 'Impossibile liberare il posto', 'error');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
await loadOffices();
|
await loadOffices();
|
||||||
await loadTeamData();
|
await loadTeamData();
|
||||||
await loadTeamData();
|
|
||||||
|
|
||||||
// Initialize Modal Logic
|
// Initialize Modal Logic
|
||||||
ModalLogic.init({
|
ModalLogic.init({
|
||||||
@@ -79,7 +78,10 @@ async function loadOffices() {
|
|||||||
if (currentUser.role !== 'admin') {
|
if (currentUser.role !== 'admin') {
|
||||||
select.style.display = 'none';
|
select.style.display = 'none';
|
||||||
// Employees stop here, Managers continue to allow auto-selection logic below
|
// Employees stop here, Managers continue to allow auto-selection logic below
|
||||||
if (currentUser.role === 'employee') return;
|
if (currentUser.role === 'employee') {
|
||||||
|
updateOfficeDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache offices list
|
// Cache offices list
|
||||||
@@ -312,7 +314,9 @@ function renderCalendar() {
|
|||||||
|
|
||||||
// Build header row
|
// Build header row
|
||||||
const dayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
|
const dayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
|
||||||
let headerHtml = '<th>Nome</th><th>Gruppo</th>';
|
const showGroup = currentUser.role === 'admin';
|
||||||
|
let headerHtml = '<th>Nome</th>';
|
||||||
|
if (showGroup) headerHtml += '<th>Gruppo</th>';
|
||||||
|
|
||||||
for (let i = 0; i < dayCount; i++) {
|
for (let i = 0; i < dayCount; i++) {
|
||||||
const date = new Date(startDate);
|
const date = new Date(startDate);
|
||||||
@@ -342,7 +346,7 @@ function renderCalendar() {
|
|||||||
|
|
||||||
// Build body rows
|
// Build body rows
|
||||||
if (teamData.length === 0) {
|
if (teamData.length === 0) {
|
||||||
body.innerHTML = `<tr><td colspan="${dayCount + 2}" class="text-center">Nessun membro del team trovato</td></tr>`;
|
body.innerHTML = `<tr><td colspan="${dayCount + (showGroup ? 2 : 1)}" class="text-center">Nessun membro del team trovato</td></tr>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,8 +365,11 @@ function renderCalendar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bodyHtml += `<tr>
|
bodyHtml += `<tr>
|
||||||
<td class="member-name" style="text-align: left; padding-left: 1rem;">${nameHtml}</td>
|
<td class="member-name" style="text-align: left; padding-left: 1rem;">${nameHtml}</td>`;
|
||||||
<td class="member-manager">${member.office_name || '-'}</td>`;
|
|
||||||
|
if (showGroup) {
|
||||||
|
bodyHtml += `<td class="member-manager">${member.office_name || '-'}</td>`;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < dayCount; i++) {
|
for (let i = 0; i < dayCount; i++) {
|
||||||
const date = new Date(startDate);
|
const date = new Date(startDate);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let offices = [];
|
let offices = [];
|
||||||
let currentOfficeId = null;
|
let currentOfficeId = null;
|
||||||
|
let currentOffice = null;
|
||||||
let officeUsers = [];
|
let officeUsers = [];
|
||||||
let currentWeeklyClosingDays = [];
|
let currentWeeklyClosingDays = [];
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
populateHourSelect();
|
||||||
await loadOffices();
|
await loadOffices();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
});
|
});
|
||||||
@@ -73,6 +75,27 @@ async function loadOfficeRules(officeId) {
|
|||||||
document.getElementById('rulesContent').style.display = 'block';
|
document.getElementById('rulesContent').style.display = 'block';
|
||||||
document.getElementById('noOfficeMessage').style.display = 'none';
|
document.getElementById('noOfficeMessage').style.display = 'none';
|
||||||
|
|
||||||
|
// Load full office object for algorithm settings
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/offices/${officeId}`);
|
||||||
|
if (response && response.ok) {
|
||||||
|
currentOffice = await response.json();
|
||||||
|
// Populate algorithm form
|
||||||
|
const modeSelect = document.getElementById('assignmentModeSelect');
|
||||||
|
if (currentOffice.booking_window_enabled === false) {
|
||||||
|
modeSelect.value = 'realtime';
|
||||||
|
} else {
|
||||||
|
modeSelect.value = currentOffice.assignment_mode || 'random';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('bookingWindowHour').value = currentOffice.booking_window_end_hour ?? 18;
|
||||||
|
document.getElementById('bookingWindowMinute').value = currentOffice.booking_window_end_minute ?? 0;
|
||||||
|
updateAlgorithmVisibility();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error loading office details:", e);
|
||||||
|
}
|
||||||
|
|
||||||
// Load users for this office (for dropdowns)
|
// Load users for this office (for dropdowns)
|
||||||
await loadOfficeUsers(officeId);
|
await loadOfficeUsers(officeId);
|
||||||
|
|
||||||
@@ -91,6 +114,67 @@ async function loadOfficeUsers(officeId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateHourSelect() {
|
||||||
|
const hourSelect = document.getElementById('bookingWindowHour');
|
||||||
|
const minuteSelect = document.getElementById('bookingWindowMinute');
|
||||||
|
if (!hourSelect || !minuteSelect) return;
|
||||||
|
|
||||||
|
hourSelect.innerHTML = '';
|
||||||
|
for (let h = 0; h < 24; h++) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = h;
|
||||||
|
option.textContent = h.toString().padStart(2, '0');
|
||||||
|
hourSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
minuteSelect.innerHTML = '';
|
||||||
|
for (let m = 0; m < 60; m++) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = m;
|
||||||
|
option.textContent = m.toString().padStart(2, '0');
|
||||||
|
minuteSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAlgorithmVisibility() {
|
||||||
|
const mode = document.getElementById('assignmentModeSelect').value;
|
||||||
|
const group = document.getElementById('cutoffTimeGroup');
|
||||||
|
if (group) group.style.display = (mode === 'realtime') ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAlgorithmSettings(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentOfficeId) return;
|
||||||
|
|
||||||
|
const btn = e.target.querySelector('button[type="submit"]');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Salvataggio...';
|
||||||
|
|
||||||
|
const mode = document.getElementById('assignmentModeSelect').value;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
assignment_mode: mode === 'realtime' ? 'random' : mode,
|
||||||
|
booking_window_enabled: mode !== 'realtime',
|
||||||
|
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/${currentOfficeId}`, data);
|
||||||
|
if (res) {
|
||||||
|
utils.showMessage('Impostazioni algoritmo salvate', 'success');
|
||||||
|
currentOffice = res;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
utils.showMessage('Errore nel salvataggio impostazioni', 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Weekly Closing Days
|
// Weekly Closing Days
|
||||||
async function loadWeeklyClosingDays(officeId) {
|
async function loadWeeklyClosingDays(officeId) {
|
||||||
const response = await api.get(`/api/offices/${officeId}/weekly-closing-days`);
|
const response = await api.get(`/api/offices/${officeId}/weekly-closing-days`);
|
||||||
@@ -426,6 +510,230 @@ function setupEventListeners() {
|
|||||||
notes: document.getElementById('exclusionNotes').value || null
|
notes: document.getElementById('exclusionNotes').value || null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Algorithm settings events
|
||||||
|
document.getElementById('assignmentModeSelect').addEventListener('change', updateAlgorithmVisibility);
|
||||||
|
document.getElementById('algorithmForm').addEventListener('submit', saveAlgorithmSettings);
|
||||||
|
|
||||||
|
// Test Tools Logic
|
||||||
|
// Set default date to tomorrow
|
||||||
|
const tomorrow = new Date();
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const testDateStart = document.getElementById('testDateStart');
|
||||||
|
if (testDateStart) testDateStart.valueAsDate = tomorrow;
|
||||||
|
|
||||||
|
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 = utils.formatDate(current);
|
||||||
|
try {
|
||||||
|
await api.post('/api/parking/run-allocation', {
|
||||||
|
date: dateStr,
|
||||||
|
office_id: currentOfficeId
|
||||||
|
});
|
||||||
|
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 = utils.formatDate(current);
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/parking/clear-assignments', {
|
||||||
|
date: dateStr,
|
||||||
|
office_id: currentOfficeId
|
||||||
|
});
|
||||||
|
if (res && res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
totalRemoved += (data.count || 0);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error clearing ${dateStr}`, e);
|
||||||
|
}
|
||||||
|
current.setDate(current.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.showMessage(`Operazione eseguita.`, 'warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearPresenceBtn = document.getElementById('clearPresenceBtn');
|
||||||
|
if (clearPresenceBtn) {
|
||||||
|
clearPresenceBtn.addEventListener('click', async () => {
|
||||||
|
if (!confirm('ATTENZIONE: Stai per eliminare TUTTI GLI STATI (Presente/Assente/ecc) e relative assegnazioni per tutti gli utenti del gruppo nel periodo selezionato. \n\nQuesta azione è irreversibile. 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');
|
||||||
|
|
||||||
|
// Validate office
|
||||||
|
if (!currentOfficeId) {
|
||||||
|
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const endDateVal = dateEnd || dateStart;
|
||||||
|
|
||||||
|
utils.showMessage('Rimozione stati in corso...', 'warning');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/presence/admin/clear-office-presence', {
|
||||||
|
start_date: dateStart,
|
||||||
|
end_date: endDateVal,
|
||||||
|
office_id: currentOfficeId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
utils.showMessage(`Operazione completata. Rimossi ${data.count_presence} stati e ${data.count_parking} assegnazioni.`, 'success');
|
||||||
|
} else {
|
||||||
|
const err = await res.json();
|
||||||
|
utils.showMessage('Errore: ' + (err.detail || 'Operazione fallita'), 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
utils.showMessage('Errore di comunicazione col server', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const testEmailBtn = document.getElementById('testEmailBtn');
|
||||||
|
if (testEmailBtn) {
|
||||||
|
testEmailBtn.addEventListener('click', async () => {
|
||||||
|
const dateVal = document.getElementById('testEmailDate').value;
|
||||||
|
|
||||||
|
// Validate office
|
||||||
|
if (!currentOfficeId) {
|
||||||
|
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.showMessage('Invio mail di test in corso...', 'warning');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/parking/test-email', {
|
||||||
|
date: dateVal || null,
|
||||||
|
office_id: currentOfficeId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status >= 200 && res.status < 300) {
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
let msg = `Email inviata con successo per la data: ${data.date}.`;
|
||||||
|
if (data.mode === 'FILE') {
|
||||||
|
msg += ' (SMTP disabilitato: Loggato su file)';
|
||||||
|
}
|
||||||
|
utils.showMessage(msg, 'success');
|
||||||
|
} else {
|
||||||
|
utils.showMessage('Invio fallito. Controlla i log del server.', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const err = res ? await res.json() : {};
|
||||||
|
console.error("Test Email Error:", err);
|
||||||
|
const errMsg = err.detail ? (typeof err.detail === 'object' ? JSON.stringify(err.detail) : err.detail) : 'Invio fallito';
|
||||||
|
utils.showMessage('Errore: ' + errMsg, 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
utils.showMessage('Errore di comunicazione col server', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bulkEmailBtn = document.getElementById('bulkEmailBtn');
|
||||||
|
if (bulkEmailBtn) {
|
||||||
|
bulkEmailBtn.addEventListener('click', async () => {
|
||||||
|
const dateVal = document.getElementById('testEmailDate').value;
|
||||||
|
|
||||||
|
// Validate office
|
||||||
|
if (!currentOfficeId) {
|
||||||
|
return utils.showMessage('Errore: Nessun gruppo selezionato', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dateVal) {
|
||||||
|
return utils.showMessage('Per il test a TUTTI è obbligatorio selezionare una data specifica.', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`Sei sicuro di voler inviare una mail di promemoria a TUTTI gli utenti con parcheggio assegnato per il giorno ${dateVal}?\n\nQuesta azione invierà vere email.`)) return;
|
||||||
|
|
||||||
|
utils.showMessage('Invio mail massive in corso...', 'warning');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/parking/test-email', {
|
||||||
|
date: dateVal,
|
||||||
|
office_id: currentOfficeId,
|
||||||
|
bulk_send: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status >= 200 && res.status < 300) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
let msg = `Processo completato per il ${data.date}. Inviate: ${data.count || 0}, Fallite: ${data.failed || 0}.`;
|
||||||
|
if (data.mode === 'BULK' && (data.count || 0) === 0) msg += " (Nessun assegnatario trovato)";
|
||||||
|
utils.showMessage(msg, 'success');
|
||||||
|
} else {
|
||||||
|
utils.showMessage('Errore durante l\'invio.', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const err = res ? await res.json() : {};
|
||||||
|
console.error("Bulk Test Email Error:", err);
|
||||||
|
const errMsg = err.detail ? (typeof err.detail === 'object' ? JSON.stringify(err.detail) : err.detail) : 'Invio fallito';
|
||||||
|
utils.showMessage('Errore: ' + errMsg, 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
utils.showMessage('Errore di comunicazione col server: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global functions
|
// Global functions
|
||||||
|
|||||||
@@ -94,6 +94,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label for="assignmentModeSelect">Metodo di Assegnazione</label>
|
||||||
|
<select id="assignmentModeSelect" class="form-control">
|
||||||
|
<option value="realtime">In Tempo Reale (FIFO)</option>
|
||||||
|
<option value="random">Automatico - Casuale (Batch)</option>
|
||||||
|
<option value="fairness">Automatico - Punteggio (Batch)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">FIFO: primo che arriva prende il posto. Automatico: assegnazione collettiva dopo il cut-off.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="cutoffGroup">
|
||||||
<label>Orario di Cut-off (Giorno Precedente)</label>
|
<label>Orario di Cut-off (Giorno Precedente)</label>
|
||||||
<div style="display: flex; gap: 10px; align-items: center;">
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
<select id="officeCutoffHour" class="form-control" style="width: 80px;">
|
<select id="officeCutoffHour" class="form-control" style="width: 80px;">
|
||||||
@@ -106,12 +116,8 @@
|
|||||||
<option value="30">30</option>
|
<option value="30">30</option>
|
||||||
<option value="45">45</option>
|
<option value="45">45</option>
|
||||||
</select>
|
</select>
|
||||||
<label style="margin-left: 10px; display: flex; align-items: center; gap: 5px;">
|
|
||||||
<input type="checkbox" id="officeWindowEnabled">
|
|
||||||
Abilita Assegnazione Automatica
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">Orario limite per la prenotazione del giorno successivo</small>
|
<small class="text-muted">Orario limite per la prenotazione del giorno successivo (solo per modalità Automatica)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
<!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 Gruppo</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: Algorithm Settings -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Impostazioni Algoritmo Parcheggio</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form id="scheduleForm">
|
|
||||||
<div class="form-group" style="padding-bottom: 1rem; border-bottom: 1px solid #e5e7eb; margin-bottom: 1rem;">
|
|
||||||
<label style="font-weight: 500; display: block; margin-bottom: 0.5rem;">Modalità Assegnazione</label>
|
|
||||||
<p class="text-muted" style="margin-bottom: 0.5rem;">Scegli la modalità con cui assegnare i posti auto ai membri del gruppo.</p>
|
|
||||||
<select id="assignmentModeSelect" class="form-select" style="max-width: 300px;">
|
|
||||||
<option value="fairness">Punteggio (Fairness)</option>
|
|
||||||
<option value="random">Completamente Random</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="toggle-label">
|
|
||||||
<span style="font-weight: 500;">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 style="font-weight: 500;">Orario di Cut-off (Giorno Precedente)</label>
|
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem;">
|
|
||||||
<select id="bookingWindowHour" style="width: 80px;">
|
|
||||||
</select>
|
|
||||||
<span>:</span>
|
|
||||||
<select id="bookingWindowMinute" style="width: 80px;">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">Le presenze inserite prima di questo orario saranno messe in attesa.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions" style="margin-top: 1.5rem;">
|
|
||||||
<button type="submit" class="btn btn-dark">Salva Impostazioni</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card: 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>
|
|
||||||
<button id="clearPresenceBtn" class="btn btn-danger"
|
|
||||||
title="Elimina stati e assegnazioni per i giorni selezionati">
|
|
||||||
Elimina Stati
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="margin: 1.5rem 0; border: 0; border-top: 1px solid #e5e7eb;">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Test Invio Email</label>
|
|
||||||
<div style="display: flex; gap: 1rem; align-items: flex-end;">
|
|
||||||
<div style="flex: 1;">
|
|
||||||
<small>Data di Riferimento (Opzionale):</small>
|
|
||||||
<input type="date" id="testEmailDate" class="form-control">
|
|
||||||
<small class="text-muted" style="display: block; margin-top: 0.25rem;">
|
|
||||||
Se non specificata, verrà usato il primo giorno lavorativo disponibile.
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<button id="testEmailBtn" class="btn btn-secondary">
|
|
||||||
Test (Solo a Me)
|
|
||||||
</button>
|
|
||||||
<button id="bulkEmailBtn" class="btn btn-warning"
|
|
||||||
title="Invia mail reale a tutti gli assegnatari">
|
|
||||||
Test (A Tutti)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
@@ -60,6 +60,42 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="rulesContent" style="display: none;">
|
<div id="rulesContent" style="display: none;">
|
||||||
|
<!-- Card: Algorithm Settings -->
|
||||||
|
<div class="card" style="margin-bottom: 1.5rem;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Impostazioni Algoritmo Parcheggio</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="algorithmForm">
|
||||||
|
<div class="form-group" style="padding-bottom: 1rem; border-bottom: 1px solid #e5e7eb; margin-bottom: 1rem;">
|
||||||
|
<label style="font-weight: 500; display: block; margin-bottom: 0.5rem;">Metodo di Assegnazione</label>
|
||||||
|
<p class="text-muted" style="margin-bottom: 0.5rem;">Scegli come assegnare i posti auto ai membri del gruppo.</p>
|
||||||
|
<select id="assignmentModeSelect" class="form-select" style="max-width: 300px;">
|
||||||
|
<option value="realtime">In Tempo Reale (FIFO)</option>
|
||||||
|
<option value="random">Automatico - Casuale (Batch)</option>
|
||||||
|
<option value="fairness">Automatico - Punteggio (Batch)</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted" style="display: block; margin-top: 0.5rem;">FIFO: assegnazione immediata. Automatico: assegnazione collettiva dopo il cut-off.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="cutoffTimeGroup">
|
||||||
|
<label style="font-weight: 500;">Orario di Cut-off (Giorno Precedente)</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem;">
|
||||||
|
<select id="bookingWindowHour" style="width: 80px;">
|
||||||
|
</select>
|
||||||
|
<span>:</span>
|
||||||
|
<select id="bookingWindowMinute" style="width: 80px;">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Orario limite per la prenotazione del giorno successivo (solo per modalità Automatica)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions" style="margin-top: 1.5rem;">
|
||||||
|
<button type="submit" class="btn btn-dark">Salva Impostazioni</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Weekly Closing Days -->
|
<!-- Weekly Closing Days -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -119,6 +155,69 @@
|
|||||||
<div id="exclusionsList" class="rules-list"></div>
|
<div id="exclusionsList" class="rules-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Card: Testing Tools -->
|
||||||
|
<div class="card" style="margin-top: 1.5rem;">
|
||||||
|
<div class="card-header"
|
||||||
|
style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<h3>Strumenti di Test</h3>
|
||||||
|
<span class="badge" style="background: #fff7ed; color: #c2410c; border: 1px solid #ffedd5; padding: 0.25rem 0.5rem; font-size: 0.75rem;">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>
|
||||||
|
<button id="clearPresenceBtn" class="btn btn-danger"
|
||||||
|
title="Elimina stati e assegnazioni per i giorni selezionati">
|
||||||
|
Elimina Stati
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin: 1.5rem 0; border: 0; border-top: 1px solid #e5e7eb;">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Test Invio Email</label>
|
||||||
|
<div style="display: flex; gap: 1rem; align-items: flex-end;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<small>Data di Riferimento (Opzionale):</small>
|
||||||
|
<input type="date" id="testEmailDate" class="form-control">
|
||||||
|
<small class="text-muted" style="display: block; margin-top: 0.25rem;">
|
||||||
|
Se non specificata, verrà usato il primo giorno lavorativo disponibile.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button id="testEmailBtn" class="btn btn-secondary">
|
||||||
|
Test (Solo a Me)
|
||||||
|
</button>
|
||||||
|
<button id="bulkEmailBtn" class="btn btn-warning"
|
||||||
|
title="Invia mail reale a tutti gli assegnatari">
|
||||||
|
Test (A Tutti)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
5
main.py
5
main.py
@@ -195,10 +195,7 @@ async def settings_page():
|
|||||||
return FileResponse(config.FRONTEND_DIR / "pages" / "settings.html")
|
return FileResponse(config.FRONTEND_DIR / "pages" / "settings.html")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/parking-settings")
|
|
||||||
async def parking_settings_page():
|
|
||||||
"""Parking Settings page"""
|
|
||||||
return FileResponse(config.FRONTEND_DIR / "pages" / "parking-settings.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/favicon.svg")
|
@app.get("/favicon.svg")
|
||||||
|
|||||||
Reference in New Issue
Block a user