diff --git a/README.md b/README.md
index eb8d8f9..9747f29 100644
--- a/README.md
+++ b/README.md
@@ -1,214 +1,214 @@
-# Org-Parking
-
-Un'applicazione leggera gestionale per i parcheggi aziendali, progettata per le organizzazioni. Offre un algoritmo di assegnazione equa, tracciamento delle presenze ed è ottimizzata per basse risorse.
-
-## Funzionalità
-
-- **Modello Centrato sull'Ufficio**: I posti auto appartengono agli Uffici, non ai Manager o agli Utenti.
-- **Algoritmo di Assegnazione Equa**: Gli utenti con il rapporto parcheggi/presenze più basso hanno la priorità.
-- **Tracciamento Presenze**: Marcatura delle presenze basata su calendario (presente/remoto/assente).
-- **Regole di Chiusura Flessibili**: Supporto per chiusure in date specifiche e chiusure settimanali ricorrenti per ufficio.
-- **Garanzie ed Esclusioni**: Regole di parcheggio per utente gestite dagli amministratori dell'ufficio.
-- **Accesso Basato sui Ruoli**:
- - **Admin**: Controllo completo del sistema, creazione uffici, gestione utenti.
- - **Manager**: Gestisce le impostazioni del proprio ufficio e il team.
- - **Impiegato**: Segna presenza, visualizza calendario, controlla stato parcheggio.
-- **Basso Consumo di Memoria**: Ottimizzato per piccoli server (<500MB RAM).
-- **Autenticazione**: Auth JWT integrata o integrazione SSO con Authelia.
-
-## Architettura
-
-```
-app/
-├── routes/ # API endpoints
-│ ├── auth.py # Autenticazione
-│ ├── users.py # Gestione utenti
-│ ├── offices.py # Gestione uffici (quote, regole)
-│ ├── presence.py # Marcatura presenze
-│ └── parking.py # Logica di assegnazione
-└── config.py # Configurazione
-database/
-├── models.py # Modelli SQLAlchemy ORM
-└── connection.py # Setup Database
-frontend/ # Frontend Vanilla JS pulito
-├── pages/ # Viste HTML
-├── js/ # Moduli logici
-└── css/ # Stili
-```
-
-## Guida Rapida
-
-### Sviluppo Locale
-
-1. **Setup Ambiente**:
- ```bash
- python3 -m venv .venv
- source .venv/bin/activate
- pip install -r requirements.txt
- ```
-
-2. **Avvio Server**:
- ```bash
- python main.py
- ```
- Accedi a `http://localhost:8000`
-
-### Deployment Docker (Consigliato)
-
-Questa applicazione è ottimizzata per ambienti con risorse limitate (es. Raspberry Pi, piccoli VPS).
-
-1. **Build**:
- ```bash
- docker compose build
- ```
-
-2. **Run**:
- ```bash
- docker compose up -d
- ```
-
- **Nota sull'Uso della Memoria**:
- Il `Dockerfile` è configurato per eseguire `uvicorn` con `--workers 1` per minimizzare l'uso della memoria (~50MB in idle). Se in esecuzione su un server più grande, puoi rimuovere questo flag nel `Dockerfile`.
-
-## Configurazione
-
-Copia `.env.example` in `.env` e configura:
-
-| Variabile | Descrizione | Default |
-|-----------|-------------|---------|
-| `SECRET_KEY` | **Richiesto**. Chiave cripto per JWT. | `""` (Esce se mancante) |
-| `DATABASE_URL` | Vedi docs SQLAlchemy. | `sqlite:///data/parking.db` |
-| `AUTHELIA_ENABLED`| Abilita supporto header SSO. | `false` |
-| `SMTP_ENABLED` | Abilita notifiche email. | `false` |
-| `LOG_LEVEL` | Verbosità log. | `INFO` |
-
-## Algoritmo di Equità
-
-I posti auto vengono assegnati giornalmente basandosi su un rapporto di equità:
-```
-Rapporto = (Giorni Parcheggiati con successo) / (Giorni di Presenza in ufficio)
-```
-- Gli utenti **Garantiti** vengono assegnati per primi.
-- I posti **Rimanenti** sono distribuiti agli utenti presenti iniziando da chi ha il rapporto più **basso**.
-- Gli utenti **Esclusi** non ricevono mai un posto.
-
-## API Endpoints
-
-Di seguito la lista delle chiamate API disponibili suddivise per modulo.
-
-### Auth (`/api/auth`)
-Gestione autenticazione e sessione.
-
-- `POST /register`: Registrazione nuovo utente (Disabilitato se Authelia è attivo).
-- `POST /login`: Login con email e password (ritorna token JWT/cookie).
-- `POST /logout`: Logout e invalidazione sessione.
-- `GET /me`: Ritorna informazioni sull'utente corrente.
-- `GET /config`: Ritorna la configurazione pubblica di autenticazione.
-- `GET /holidays/{year}`: Ritorna i giorni festivi per l'anno specificato.
-
-### Users (`/api/users`)
-Gestione utenti e profili.
-
-- `GET /`: Lista di tutti gli utenti (Solo Admin).
-- `POST /`: Crea un nuovo utente (Solo Admin).
-- `GET /{user_id}`: Dettaglio di un utente specifico (Solo Admin).
-- `PUT /{user_id}`: Aggiorna dati di un utente (Solo Admin).
-- `DELETE /{user_id}`: Elimina un utente (Solo Admin).
-- `GET /me/profile`: Ottieni il proprio profilo.
-- `PUT /me/profile`: Aggiorna il proprio profilo.
-- `GET /me/settings`: Ottieni le proprie impostazioni.
-- `PUT /me/settings`: Aggiorna le proprie impostazioni.
-- `POST /me/change-password`: Modifica la propria password.
-
-### Offices (`/api/offices`)
-Gestione uffici, regole di chiusura e quote.
-
-- `GET /`: Lista di tutti gli uffici.
-- `POST /`: Crea un nuovo ufficio (Solo Admin).
-- `GET /{office_id}`: Dettagli di un ufficio.
-- `PUT /{office_id}`: Aggiorna configurazione ufficio (Solo Admin).
-- `DELETE /{office_id}`: Elimina un ufficio (Solo Admin).
-- `GET /{office_id}/users`: Lista utenti assegnati all'ufficio.
-- `GET /{office_id}/closing-days`: Lista giorni di chiusura specifici.
-- `POST /{office_id}/closing-days`: Aggiungi giorno di chiusura.
-- `DELETE /{office_id}/closing-days/{id}`: Rimuovi giorno di chiusura.
-- `GET /{office_id}/weekly-closing-days`: Lista giorni di chiusura settimanali (es. Sabato/Domenica).
-- `POST /{office_id}/weekly-closing-days`: Aggiungi giorno di chiusura settimanale.
-- `DELETE /{office_id}/weekly-closing-days/{id}`: Rimuovi giorno di chiusura settimanale.
-- `GET /{office_id}/guarantees`: Lista utenti con posto garantito.
-- `POST /{office_id}/guarantees`: Aggiungi garanzia posto.
-- `DELETE /{office_id}/guarantees/{id}`: Rimuovi garanzia.
-- `GET /{office_id}/exclusions`: Lista utenti esclusi dal parcheggio.
-- `POST /{office_id}/exclusions`: Aggiungi esclusione.
-- `DELETE /{office_id}/exclusions/{id}`: Rimuovi esclusione.
-
-### Presence (`/api/presence`)
-Gestione presenze giornaliere.
-
-- `POST /mark`: Segna la propria presenza per una data (Presente/Remoto/Assente).
-- `GET /my-presences`: Lista delle proprie presenze.
-- `DELETE /{date}`: Rimuovi la propria presenza per una data.
-- `POST /admin/mark`: Segna presenza per un altro utente (Manager/Admin).
-- `DELETE /admin/{user_id}/{date}`: Rimuovi presenza di un altro utente (Manager/Admin).
-- `GET /team`: Visualizza presenze e stato parcheggio del team.
-- `GET /admin/{user_id}`: Storico presenze di un utente.
-
-### Parking (`/api/parking`)
-Gestione assegnazioni posti auto.
-
-- `POST /init-office-pool`: Inizializza i posti disponibili per un giorno.
-- `GET /assignments/{date}`: Lista assegnazioni per una data.
-- `GET /my-assignments`: Le mie assegnazioni parcheggio.
-- `POST /run-allocation`: Esegui manualmente l'algoritmo di assegnazione per una data.
-- `POST /clear-assignments`: Cancella tutte le assegnazioni per una data.
-- `POST /manual-assign`: Assegna manualmente un posto a un utente.
-- `POST /reassign-spot`: Riassegna o libera un posto già assegnato.
-- `POST /release-my-spot/{id}`: Rilascia il proprio posto assegnato.
-- `GET /eligible-users/{id}`: Lista utenti idonei a ricevere un posto riassegnato.
-
-## Utilizzo con AUTHELIA
-
-Org-Parking supporta l'integrazione nativa con **Authelia** (o altri provider SSO compatibili con Forward Auth). Questo permette il Single Sign-On (SSO) e la gestione centralizzata degli utenti.
-
-### Configurazione
-
-1. **Abilita Authelia**:
- Nel file `.env`, imposta `AUTHELIA_ENABLED=true`.
-
-2. **Configura gli Header del Proxy**:
- Assicurati che il tuo reverse proxy (es. Traefik, Nginx) passi i seguenti header all'applicazione dopo l'autenticazione:
- * `Remote-User`: Username dell'utente (spesso uguale all'email).
- * `Remote-Email`: Email dell'utente.
- * `Remote-Name`: Nome completo dell'utente (Opzionale).
- * `Remote-Groups`: Gruppi di appartenenza (separati da virgola).
-
-3. **Gestione Admin**:
- L'applicazione assegna automaticamente il ruolo di **Admin** agli utenti che appartengono al gruppo specificato nella variabile `AUTHELIA_ADMIN_GROUP` (default: `parking_admins`).
- * Se un utente viene rimosso da questo gruppo su Authelia, perderà i privilegi di admin al login successivo.
- * Gli altri ruoli (Manager, Employee) e l'assegnazione agli uffici vengono gestiti manualmente all'interno di Org-Parking da un amministratore.
-
-### Comportamento
-
-* **Creazione Utenti**: Gli utenti vengono creati automaticamente nel database di Org-Parking al loro primo accesso riuscito tramite Authelia.
-* **Login/Logout**: Le pagine di login e registrazione interne vengono disabilitate o reindirizzano al portale SSO.
-* **Password**: Org-Parking non gestisce le password in questa modalità; l'autenticazione è interamente delegata al provider esterno.
-* **Sicurezza**: L'applicazione si fida degli header `Remote-*` solo se `AUTHELIA_ENABLED` è attivo. Assicurarsi che il container non sia esposto direttamente a internet senza passare per il proxy di autenticazione.
-
-## Note di Deployment
-
-- **Database**: SQLite è usato di default per semplicità e basso overhead. I dati persistono in `./data/parking.db`.
-- **Sicurezza**:
- - Rate limiting è attivo sugli endpoint sensibili (Login/Register).
- - Le password sono hashate con Bcrypt.
- - L'autenticazione via cookie è sicura di default.
-
-### Risoluzione Problemi Comuni
-
-**Errore: "Il reindirizzamento è stato determinato per essere non sicuro"**
-
-Questo errore appare se si tenta di accedere all'applicazione tramite HTTP (es. `http://lvh.me:8000`) mentre Authelia è su HTTPS. Authelia blocca i reindirizzamenti verso protocolli non sicuri.
-**Soluzione**: Accedere all'applicazione tramite il Reverse Proxy (Caddy) usando HTTPS, ad esempio `https://parking.lvh.me`. Assicurarsi che Caddy sia configurato per gestire il dominio e l'autenticazione.
-
-## Licenza
-
-MIT
+# Org-Parking
+
+Un'applicazione leggera gestionale per i parcheggi aziendali, progettata per le organizzazioni. Offre un algoritmo di assegnazione equa, tracciamento delle presenze ed è ottimizzata per basse risorse.
+
+## Funzionalità
+
+- **Modello Centrato sull'Ufficio**: I posti auto appartengono agli Uffici, non ai Manager o agli Utenti.
+- **Algoritmo di Assegnazione Equa**: Gli utenti con il rapporto parcheggi/presenze più basso hanno la priorità.
+- **Tracciamento Presenze**: Marcatura delle presenze basata su calendario (presente/remoto/assente).
+- **Regole di Chiusura Flessibili**: Supporto per chiusure in date specifiche e chiusure settimanali ricorrenti per ufficio.
+- **Garanzie ed Esclusioni**: Regole di parcheggio per utente gestite dagli amministratori dell'ufficio.
+- **Accesso Basato sui Ruoli**:
+ - **Admin**: Controllo completo del sistema, creazione uffici, gestione utenti.
+ - **Manager**: Gestisce le impostazioni del proprio ufficio e il team.
+ - **Impiegato**: Segna presenza, visualizza calendario, controlla stato parcheggio.
+- **Basso Consumo di Memoria**: Ottimizzato per piccoli server (<500MB RAM).
+- **Autenticazione**: Auth JWT integrata o integrazione SSO con Authelia.
+
+## Architettura
+
+```
+app/
+├── routes/ # API endpoints
+│ ├── auth.py # Autenticazione
+│ ├── users.py # Gestione utenti
+│ ├── offices.py # Gestione uffici (quote, regole)
+│ ├── presence.py # Marcatura presenze
+│ └── parking.py # Logica di assegnazione
+└── config.py # Configurazione
+database/
+├── models.py # Modelli SQLAlchemy ORM
+└── connection.py # Setup Database
+frontend/ # Frontend Vanilla JS pulito
+├── pages/ # Viste HTML
+├── js/ # Moduli logici
+└── css/ # Stili
+```
+
+## Guida Rapida
+
+### Sviluppo Locale
+
+1. **Setup Ambiente**:
+ ```bash
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install -r requirements.txt
+ ```
+
+2. **Avvio Server**:
+ ```bash
+ python main.py
+ ```
+ Accedi a `http://localhost:8000`
+
+### Deployment Docker (Consigliato)
+
+Questa applicazione è ottimizzata per ambienti con risorse limitate (es. Raspberry Pi, piccoli VPS).
+
+1. **Build**:
+ ```bash
+ docker compose build
+ ```
+
+2. **Run**:
+ ```bash
+ docker compose up -d
+ ```
+
+ **Nota sull'Uso della Memoria**:
+ Il `Dockerfile` è configurato per eseguire `uvicorn` con `--workers 1` per minimizzare l'uso della memoria (~50MB in idle). Se in esecuzione su un server più grande, puoi rimuovere questo flag nel `Dockerfile`.
+
+## Configurazione
+
+Copia `.env.example` in `.env` e configura:
+
+| Variabile | Descrizione | Default |
+|-----------|-------------|---------|
+| `SECRET_KEY` | **Richiesto**. Chiave cripto per JWT. | `""` (Esce se mancante) |
+| `DATABASE_URL` | Vedi docs SQLAlchemy. | `sqlite:///data/parking.db` |
+| `AUTHELIA_ENABLED`| Abilita supporto header SSO. | `false` |
+| `SMTP_ENABLED` | Abilita notifiche email. | `false` |
+| `LOG_LEVEL` | Verbosità log. | `INFO` |
+
+## Algoritmo di Equità
+
+I posti auto vengono assegnati giornalmente basandosi su un rapporto di equità:
+```
+Rapporto = (Giorni Parcheggiati con successo) / (Giorni di Presenza in ufficio)
+```
+- Gli utenti **Garantiti** vengono assegnati per primi.
+- I posti **Rimanenti** sono distribuiti agli utenti presenti iniziando da chi ha il rapporto più **basso**.
+- Gli utenti **Esclusi** non ricevono mai un posto.
+
+## API Endpoints
+
+Di seguito la lista delle chiamate API disponibili suddivise per modulo.
+
+### Auth (`/api/auth`)
+Gestione autenticazione e sessione.
+
+- `POST /register`: Registrazione nuovo utente (Disabilitato se Authelia è attivo).
+- `POST /login`: Login con email e password (ritorna token JWT/cookie).
+- `POST /logout`: Logout e invalidazione sessione.
+- `GET /me`: Ritorna informazioni sull'utente corrente.
+- `GET /config`: Ritorna la configurazione pubblica di autenticazione.
+- `GET /holidays/{year}`: Ritorna i giorni festivi per l'anno specificato.
+
+### Users (`/api/users`)
+Gestione utenti e profili.
+
+- `GET /`: Lista di tutti gli utenti (Solo Admin).
+- `POST /`: Crea un nuovo utente (Solo Admin).
+- `GET /{user_id}`: Dettaglio di un utente specifico (Solo Admin).
+- `PUT /{user_id}`: Aggiorna dati di un utente (Solo Admin).
+- `DELETE /{user_id}`: Elimina un utente (Solo Admin).
+- `GET /me/profile`: Ottieni il proprio profilo.
+- `PUT /me/profile`: Aggiorna il proprio profilo.
+- `GET /me/settings`: Ottieni le proprie impostazioni.
+- `PUT /me/settings`: Aggiorna le proprie impostazioni.
+- `POST /me/change-password`: Modifica la propria password.
+
+### Offices (`/api/offices`)
+Gestione uffici, regole di chiusura e quote.
+
+- `GET /`: Lista di tutti gli uffici.
+- `POST /`: Crea un nuovo ufficio (Solo Admin).
+- `GET /{office_id}`: Dettagli di un ufficio.
+- `PUT /{office_id}`: Aggiorna configurazione ufficio (Solo Admin).
+- `DELETE /{office_id}`: Elimina un ufficio (Solo Admin).
+- `GET /{office_id}/users`: Lista utenti assegnati all'ufficio.
+- `GET /{office_id}/closing-days`: Lista giorni di chiusura specifici.
+- `POST /{office_id}/closing-days`: Aggiungi giorno di chiusura.
+- `DELETE /{office_id}/closing-days/{id}`: Rimuovi giorno di chiusura.
+- `GET /{office_id}/weekly-closing-days`: Lista giorni di chiusura settimanali (es. Sabato/Domenica).
+- `POST /{office_id}/weekly-closing-days`: Aggiungi giorno di chiusura settimanale.
+- `DELETE /{office_id}/weekly-closing-days/{id}`: Rimuovi giorno di chiusura settimanale.
+- `GET /{office_id}/guarantees`: Lista utenti con posto garantito.
+- `POST /{office_id}/guarantees`: Aggiungi garanzia posto.
+- `DELETE /{office_id}/guarantees/{id}`: Rimuovi garanzia.
+- `GET /{office_id}/exclusions`: Lista utenti esclusi dal parcheggio.
+- `POST /{office_id}/exclusions`: Aggiungi esclusione.
+- `DELETE /{office_id}/exclusions/{id}`: Rimuovi esclusione.
+
+### Presence (`/api/presence`)
+Gestione presenze giornaliere.
+
+- `POST /mark`: Segna la propria presenza per una data (Presente/Remoto/Assente).
+- `GET /my-presences`: Lista delle proprie presenze.
+- `DELETE /{date}`: Rimuovi la propria presenza per una data.
+- `POST /admin/mark`: Segna presenza per un altro utente (Manager/Admin).
+- `DELETE /admin/{user_id}/{date}`: Rimuovi presenza di un altro utente (Manager/Admin).
+- `GET /team`: Visualizza presenze e stato parcheggio del team.
+- `GET /admin/{user_id}`: Storico presenze di un utente.
+
+### Parking (`/api/parking`)
+Gestione assegnazioni posti auto.
+
+- `POST /init-office-pool`: Inizializza i posti disponibili per un giorno.
+- `GET /assignments/{date}`: Lista assegnazioni per una data.
+- `GET /my-assignments`: Le mie assegnazioni parcheggio.
+- `POST /run-allocation`: Esegui manualmente l'algoritmo di assegnazione per una data.
+- `POST /clear-assignments`: Cancella tutte le assegnazioni per una data.
+- `POST /manual-assign`: Assegna manualmente un posto a un utente.
+- `POST /reassign-spot`: Riassegna o libera un posto già assegnato.
+- `POST /release-my-spot/{id}`: Rilascia il proprio posto assegnato.
+- `GET /eligible-users/{id}`: Lista utenti idonei a ricevere un posto riassegnato.
+
+## Utilizzo con AUTHELIA
+
+Org-Parking supporta l'integrazione nativa con **Authelia** (o altri provider SSO compatibili con Forward Auth). Questo permette il Single Sign-On (SSO) e la gestione centralizzata degli utenti.
+
+### Configurazione
+
+1. **Abilita Authelia**:
+ Nel file `.env`, imposta `AUTHELIA_ENABLED=true`.
+
+2. **Configura gli Header del Proxy**:
+ Assicurati che il tuo reverse proxy (es. Traefik, Nginx) passi i seguenti header all'applicazione dopo l'autenticazione:
+ * `Remote-User`: Username dell'utente (spesso uguale all'email).
+ * `Remote-Email`: Email dell'utente.
+ * `Remote-Name`: Nome completo dell'utente (Opzionale).
+ * `Remote-Groups`: Gruppi di appartenenza (separati da virgola).
+
+3. **Gestione Admin**:
+ L'applicazione assegna automaticamente il ruolo di **Admin** agli utenti che appartengono al gruppo specificato nella variabile `AUTHELIA_ADMIN_GROUP` (default: `parking_admins`).
+ * Se un utente viene rimosso da questo gruppo su Authelia, perderà i privilegi di admin al login successivo.
+ * Gli altri ruoli (Manager, Employee) e l'assegnazione agli uffici vengono gestiti manualmente all'interno di Org-Parking da un amministratore.
+
+### Comportamento
+
+* **Creazione Utenti**: Gli utenti vengono creati automaticamente nel database di Org-Parking al loro primo accesso riuscito tramite Authelia.
+* **Login/Logout**: Le pagine di login e registrazione interne vengono disabilitate o reindirizzano al portale SSO.
+* **Password**: Org-Parking non gestisce le password in questa modalità; l'autenticazione è interamente delegata al provider esterno.
+* **Sicurezza**: L'applicazione si fida degli header `Remote-*` solo se `AUTHELIA_ENABLED` è attivo. Assicurarsi che il container non sia esposto direttamente a internet senza passare per il proxy di autenticazione.
+
+## Note di Deployment
+
+- **Database**: SQLite è usato di default per semplicità e basso overhead. I dati persistono in `./data/parking.db`.
+- **Sicurezza**:
+ - Rate limiting è attivo sugli endpoint sensibili (Login/Register).
+ - Le password sono hashate con Bcrypt.
+ - L'autenticazione via cookie è sicura di default.
+
+### Risoluzione Problemi Comuni
+
+**Errore: "Il reindirizzamento è stato determinato per essere non sicuro"**
+
+Questo errore appare se si tenta di accedere all'applicazione tramite HTTP (es. `http://lvh.me:8000`) mentre Authelia è su HTTPS. Authelia blocca i reindirizzamenti verso protocolli non sicuri.
+**Soluzione**: Accedere all'applicazione tramite il Reverse Proxy (Caddy) usando HTTPS, ad esempio `https://parking.lvh.me`. Assicurarsi che Caddy sia configurato per gestire il dominio e l'autenticazione.
+
+## Licenza
+
+MIT
diff --git a/app/routes/offices.py b/app/routes/offices.py
index fac42dd..169f48f 100644
--- a/app/routes/offices.py
+++ b/app/routes/offices.py
@@ -26,10 +26,10 @@ router = APIRouter(prefix="/api/offices", tags=["offices"])
class ValidOfficeCreate(BaseModel):
name: str
parking_quota: int = 0
- booking_window_enabled: bool = True
+ booking_window_enabled: bool = False
booking_window_end_hour: int = 18
booking_window_end_minute: int = 0
- assignment_mode: str = "fairness"
+ assignment_mode: str = "random"
class ClosingDayCreate(BaseModel):
@@ -92,7 +92,11 @@ def list_offices(db: Session = Depends(get_db), user=Depends(require_manager_or_
"name": office.name,
"parking_quota": office.parking_quota,
"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
]
diff --git a/frontend/js/admin-offices.js b/frontend/js/admin-offices.js
index 1c600a6..26ad837 100644
--- a/frontend/js/admin-offices.js
+++ b/frontend/js/admin-offices.js
@@ -7,6 +7,7 @@ let currentUser = null;
let offices = [];
document.addEventListener('DOMContentLoaded', async () => {
+ populateTimeSelects();
currentUser = await api.requireAuth();
if (!currentUser) return;
@@ -17,7 +18,6 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadOffices();
setupEventListeners();
- populateTimeSelects();
});
function populateTimeSelects() {
@@ -48,7 +48,7 @@ function populateTimeSelects() {
}
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) {
offices = await response.json();
renderOffices();
@@ -98,14 +98,30 @@ async function editOffice(officeId) {
document.getElementById('officeName').value = office.name;
document.getElementById('officeQuota').value = office.parking_quota;
- // Set booking window settings
- document.getElementById('officeWindowEnabled').checked = office.booking_window_enabled !== false;
+ // Set assignment mode mapping
+ 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('officeCutoffMinute').value = office.booking_window_end_minute != null ? office.booking_window_end_minute : 0;
+ updateCutoffVisibility();
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) {
const office = offices.find(o => o.id === officeId);
if (!office) return;
@@ -143,6 +159,9 @@ function setupEventListeners() {
// Form submit
const form = document.getElementById('officeForm');
form.addEventListener('submit', handleOfficeSubmit);
+
+ // Assignment mode change
+ document.getElementById('assignmentModeSelect').addEventListener('change', updateCutoffVisibility);
}
async function handleOfficeSubmit(e) {
@@ -155,12 +174,15 @@ async function handleOfficeSubmit(e) {
saveBtn.innerHTML = 'Salvataggio...';
const officeId = document.getElementById('officeId').value;
+ const mode = document.getElementById('assignmentModeSelect').value;
+
const data = {
name: document.getElementById('officeName').value,
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_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);
diff --git a/frontend/js/modal-logic.js b/frontend/js/modal-logic.js
index 4d0cee8..cb3fa89 100644
--- a/frontend/js/modal-logic.js
+++ b/frontend/js/modal-logic.js
@@ -75,7 +75,7 @@ const ModalLogic = {
},
openModal(data) {
- const { dateStr, userName, presence, parking, userId, isReadOnly } = data;
+ const { dateStr, userName, presence, parking, userId, isParkingOnly } = data;
this.currentDate = dateStr;
this.currentUserId = userId; // Optional, for team view
@@ -84,8 +84,9 @@ const ModalLogic = {
const modal = document.getElementById('dayModal');
const title = document.getElementById('dayModalTitle');
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)
if (userName && userLabel) {
@@ -95,22 +96,26 @@ const ModalLogic = {
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';
+ // Presence Logic
+ if (isParkingOnly) {
+ if (statusButtons) statusButtons.style.display = 'none';
+ if (clearBtn) clearBtn.style.display = 'none';
+ title.textContent = `Gestione Parcheggio - ${utils.formatDateDisplay(dateStr)}`;
} 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
@@ -122,7 +127,8 @@ const ModalLogic = {
if (parking) {
parkingSection.style.display = 'block';
const spotName = parking.spot_display_name || parking.spot_id;
- parkingInfo.innerHTML = `Parcheggio: Posto ${spotName}`;
+ const occupantInfo = userName ? ` (Occupato da ${userName})` : '';
+ parkingInfo.innerHTML = `Posto ${spotName} ${occupantInfo}`;
} else {
parkingSection.style.display = 'none';
}
diff --git a/frontend/js/nav.js b/frontend/js/nav.js
index 3e3fd95..f724c0a 100644
--- a/frontend/js/nav.js
+++ b/frontend/js/nav.js
@@ -64,8 +64,7 @@ const NAV_ITEMS = [
{ href: '/team-calendar', icon: 'users', label: 'Calendario del team' },
{ href: '/team-rules', icon: 'rules', label: 'Regole parcheggio', roles: ['admin', 'manager'] },
{ href: '/admin/users', icon: 'user', label: 'Gestione Utenti', roles: ['admin'] },
- { href: '/admin/offices', icon: 'building', label: 'Gestione Gruppi', roles: ['admin'] },
- { href: '/parking-settings', icon: 'settings', label: 'Impostazioni Gruppi', roles: ['admin', 'manager'] }
+ { href: '/admin/offices', icon: 'building', label: 'Gestione Gruppi', roles: ['admin'] }
];
function getIcon(name) {
diff --git a/frontend/js/parking-settings.js b/frontend/js/parking-settings.js
deleted file mode 100644
index 6138fe0..0000000
--- a/frontend/js/parking-settings.js
+++ /dev/null
@@ -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');
- }
- });
- }
-}
diff --git a/frontend/js/presence.js b/frontend/js/presence.js
index 536400a..31b9e2b 100644
--- a/frontend/js/presence.js
+++ b/frontend/js/presence.js
@@ -234,36 +234,44 @@ async function handleClearPresence(date) {
async function handleReleaseParking(assignmentId) {
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) {
- await loadParkingAssignments();
+ utils.showMessage('Posto liberato con successo', 'success');
+ await Promise.all([loadParkingAssignments(), loadDailyStatus()]);
renderCalendar();
ModalLogic.closeModal();
} else {
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) {
// Basic validation handled by select; confirm
if (!assignmentId || !newUserId) {
- alert('Seleziona un utente');
+ utils.showMessage('Seleziona un utente', 'error');
return;
}
+ utils.showMessage('Riassegnazione in corso...', 'warning');
const response = await api.post('/api/parking/reassign-spot', {
assignment_id: assignmentId,
new_user_id: newUserId
});
if (response && response.ok) {
- await loadParkingAssignments();
+ utils.showMessage('Posto riassegnato con successo', 'success');
+ await Promise.all([loadParkingAssignments(), loadDailyStatus()]);
renderCalendar();
ModalLogic.closeModal();
} else {
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.addEventListener('mouseenter', () => el.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)');
el.addEventListener('mouseleave', () => el.style.boxShadow = 'none');
- el.title = "Clicca per liberare questo posto";
- el.addEventListener('click', async () => {
- if (!confirm(`Vuoi liberare il posto ${spotName} occupato da ${statusText}?`)) return;
-
- utils.showMessage('Rilascio in corso...', 'warning');
- const response = await api.post('/api/parking/reassign-spot', {
- assignment_id: a.id,
- new_user_id: null
+ el.title = "Clicca per gestire questo posto";
+ el.addEventListener('click', () => {
+ ModalLogic.openModal({
+ dateStr: a.date,
+ parking: a,
+ userName: a.user_name,
+ isParkingOnly: true
});
-
- 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');
- }
});
}
diff --git a/frontend/js/team-calendar.js b/frontend/js/team-calendar.js
index c2c648f..fec4636 100644
--- a/frontend/js/team-calendar.js
+++ b/frontend/js/team-calendar.js
@@ -26,7 +26,6 @@ document.addEventListener('DOMContentLoaded', async () => {
await loadOffices();
await loadTeamData();
- await loadTeamData();
// Initialize Modal Logic
ModalLogic.init({
@@ -79,7 +78,10 @@ async function loadOffices() {
if (currentUser.role !== 'admin') {
select.style.display = 'none';
// 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
@@ -312,7 +314,9 @@ function renderCalendar() {
// Build header row
const dayNames = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'];
- let headerHtml = '
Nome Gruppo ';
+ const showGroup = currentUser.role === 'admin';
+ let headerHtml = 'Nome ';
+ if (showGroup) headerHtml += 'Gruppo ';
for (let i = 0; i < dayCount; i++) {
const date = new Date(startDate);
@@ -342,7 +346,7 @@ function renderCalendar() {
// Build body rows
if (teamData.length === 0) {
- body.innerHTML = `Nessun membro del team trovato `;
+ body.innerHTML = `Nessun membro del team trovato `;
return;
}
@@ -361,8 +365,11 @@ function renderCalendar() {
}
bodyHtml += `
- ${nameHtml}
- ${member.office_name || '-'} `;
+ ${nameHtml} `;
+
+ if (showGroup) {
+ bodyHtml += `${member.office_name || '-'} `;
+ }
for (let i = 0; i < dayCount; i++) {
const date = new Date(startDate);
diff --git a/frontend/js/team-rules.js b/frontend/js/team-rules.js
index ca900cf..f6b346c 100644
--- a/frontend/js/team-rules.js
+++ b/frontend/js/team-rules.js
@@ -7,6 +7,7 @@
let currentUser = null;
let offices = [];
let currentOfficeId = null;
+let currentOffice = null;
let officeUsers = [];
let currentWeeklyClosingDays = [];
@@ -20,6 +21,7 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
+ populateHourSelect();
await loadOffices();
setupEventListeners();
});
@@ -73,6 +75,27 @@ async function loadOfficeRules(officeId) {
document.getElementById('rulesContent').style.display = 'block';
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)
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
async function loadWeeklyClosingDays(officeId) {
const response = await api.get(`/api/offices/${officeId}/weekly-closing-days`);
@@ -426,6 +510,230 @@ function setupEventListeners() {
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
diff --git a/frontend/pages/admin-offices.html b/frontend/pages/admin-offices.html
index b5879ae..b76bf89 100644
--- a/frontend/pages/admin-offices.html
+++ b/frontend/pages/admin-offices.html
@@ -94,6 +94,16 @@
+ Metodo di Assegnazione
+
+ In Tempo Reale (FIFO)
+ Automatico - Casuale (Batch)
+ Automatico - Punteggio (Batch)
+
+ FIFO: primo che arriva prende il posto. Automatico: assegnazione collettiva dopo il cut-off.
+
+
+
diff --git a/frontend/pages/parking-settings.html b/frontend/pages/parking-settings.html
deleted file mode 100644
index 661006a..0000000
--- a/frontend/pages/parking-settings.html
+++ /dev/null
@@ -1,183 +0,0 @@
-
-
-
-
-
-
- Office Settings - Parking Manager
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Seleziona
- Ufficio:
-
- Seleziona Ufficio
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Usa questi strumenti per verificare il
- funzionamento dell'assegnazione automatica.
-
-
-
-
-
- Esegui Assegnazione Ora
-
-
- Elimina Tutte le Assegnazioni
-
-
- Elimina Stati
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/pages/team-rules.html b/frontend/pages/team-rules.html
index 71e1677..bf66b11 100644
--- a/frontend/pages/team-rules.html
+++ b/frontend/pages/team-rules.html
@@ -60,6 +60,42 @@
+
+
+
+
+
+
Usa questi strumenti per verificare il
+ funzionamento dell'assegnazione automatica.
+
+
+
+
+
+ Esegui Assegnazione Ora
+
+
+ Elimina Tutte le Assegnazioni
+
+
+ Elimina Stati
+
+
+
+
+
+
+
+
diff --git a/main.py b/main.py
index 948fdd9..cc382e2 100644
--- a/main.py
+++ b/main.py
@@ -195,10 +195,7 @@ async def settings_page():
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")