Perfetto, Marco. Ti preparo un “task brief” chiaro e operativo da dare a Replit (o ad un dev) così implementano senza ambiguità. Puoi incollarlo così com’è. --- # Task: Vista “Pianificazione Operativa” con disponibilità per **data** ## Obiettivo Realizzare nella pagina **Pianificazione Operativa**: 1. Un **selettore data** che filtra e calcola **disponibilità** di automezzi e agenti **alla data selezionata**. 2. Un’area centrale **divisa in due colonne**: * **Sinistra**: automezzi (ordinati per disponibilità). * **Destra**: agenti (ordinati per disponibilità + priorità CCNL). 3. Ogni card (mezzo/agente) mostra **stato**: “Disponibile” (in alto) oppure “Non disponibile: assegnato a Turno #ID/Servizio”. 4. **Pulsante “Assegna servizio”** su ciascuna card, che apre il flusso d’assegnazione (mezzo/agente → servizio/turno alla data). 5. Per gli **agenti**: l’ordinamento “disponibili” deve privilegiare chi ha **meno ore già programmate** nel periodo di riferimento e **rispetta i vincoli CCNL**. --- ## Definizioni rapide * **Data di riferimento D**: la data impostata dal selettore (default = oggi). * **Periodo contabile**: ISO week della data D (lun–dom) **configurabile** (`settings.ccnl.reference_period = 'week'|'month'`); default = settimana corrente. * **Turno**: intervallo con inizio/fine, collegato a un servizio e a risorse (agenti/mezzi). * **Disponibilità**: * Mezzo disponibile se **non assegnato** in alcun turno che **intersechi D** e non in manutenzione/fermo. * Agente disponibile se **non in turno** in D e se l’assegnazione proposta **rispetta i vincoli CCNL** (vedi sotto). --- ## Vincoli CCNL (parametrizzabili) Creare un modulo `ccnlRules` con parametri in tabella/config: ``` max_hours_per_week: 48 max_hours_per_month: 190 min_rest_hours_between_shifts: 11 max_night_shifts_consecutive: 6 max_days_consecutive: 6 min_weekly_rest_hours: 24 overtime_threshold_week: 40 penalty_if_overtime: true ``` > NB: i valori sono **segnaposto**; leggere da `settings_ccnl` per cliente. Se non presenti, usare default. **Funzione chiave**: `ccnlRules.isEligible(agentId, proposedShift, dateD) -> {eligible: boolean, reasons: string[]}` Controlli minimi: * Riposo minimo tra fine ultimo turno e inizio `proposedShift.start` ≥ `min_rest_hours_between_shifts`. * Ore accumulate nel periodo di riferimento + ore del turno proposto ≤ limiti (`max_hours_per_week`/`month`). * Vincoli su notturno/consecutività/giorni consecutivi/rest settimanale (se dati). --- ## Ordinamento ### Automezzi (colonna sinistra) 1. **Disponibili** in alto, poi **Non disponibili**. 2. Tra i disponibili: opzionale secondario per **priorità mezzo** (es. min km, categoria, stato batteria/serbatoio se disponibile). 3. Tra i non disponibili: ordina per **ora di fine del turno corrente** (prima quelli che si liberano prima). ### Agenti (colonna destra) 1. **Disponibili & CCNL-eligible** in alto. 2. Ordinare i disponibili per: * **OreTotaliPeriodo (crescente)** = somma ore dei turni dell’agente nel periodo di riferimento della data D. * In caso di pari ore: meno straordinari > più straordinari; poi alfabetico. 3. Poi **Non disponibili** con motivazione (es. “Assegnato a Turno #123 08:00–16:00”). --- ## UI/UX (wireframe funzionale) * Header: `DatePicker [D]` + (opz) `Periodo: settimana/mese` (solo admin). * Body: **Grid 2 colonne responsive** (>= md: 2 colonne; < md: stack). * **Colonna Sinistra – Automezzi** * Card Mezzo: `targa | modello | stato` * Badge: `Disponibile` / `Non disponibile – Turno #ID hh:mm–hh:mm (Servizio)` * (se non disponibile) small link “vedi turno/servizio” * CTA: **Assegna servizio** * **Colonna Destra – Agenti** * Card Agente: `cognome nome | qualifica` * Badge: `Disponibile` / `Non disponibile – Turno #ID` * Righe info: `Ore settimana: Xh`, `Straordinari: Yh`, eventuali warning CCNL (tooltip) * CTA: **Assegna servizio** * Click “Assegna servizio” → apre **modal**: * Se chiamato da un **mezzo**: pre-seleziona mezzo, mostra servizi/turni disponibili in D, scegli agente. * Se chiamato da un **agente**: pre-seleziona agente, mostra servizi/turni disponibili in D, scegli mezzo. * **Validazione live CCNL**: mostra `✅ Conforme` o `❌ Motivi`. --- ## Modello dati (minimo) ```ts // Tabella veicoli Vehicle { id, plate, model, status: 'active'|'maintenance'|'out', priority?: number } // Tabella agenti Agent { id, first_name, last_name, qualification, enabled: boolean } // Turni (calendar) Shift { id, service_id, start_at: datetime, end_at: datetime, assigned_agent_id?: id, assigned_vehicle_id?: id } // Servizi Service { id, name, site, notes } // Parametri CCNL (per tenant/cliente) SettingsCCNL { key, value } ``` --- ## API (contratti) * `GET /planning?date=YYYY-MM-DD` * Ritorna: ```json { "date": "2025-10-17", "vehicles": [ { "id": 12, "plate": "AB123CD", "status": "available"|"busy", "busy_shift": { "id": 77, "start_at": "...", "end_at": "...", "service_name": "..." } | null } ], "agents": [ { "id": 9, "name": "Rossi Mario", "status": "available"|"busy", "ccnl_eligible": true|false, "hours_period": 28.0, "overtime_hours": 3.0, "busy_shift": { ... } | null, "ccnl_notes": ["min rest ok", "within weekly cap"] } ] } ``` * `POST /assign` * Body: ```json { "date": "YYYY-MM-DD", "service_id": 10, "shift": {"start_at":"...","end_at":"..."}, "agent_id": 9, "vehicle_id": 12 } ``` * Validazioni: overlap, veicolo libero, `ccnlRules.isEligible(...)`. * Risposte: `200 {ok:true, created_shift_id}` | `422 {ok:false, errors:[...]}` * `GET /ccnl/preview?agent_id=&start_at=&end_at=` * Ritorna esito eleggibilità e motivi. --- ## Logica (pseudocodice) ```pseudo function getPlanning(date D): period = resolvePeriod(D, settings.ccnl.reference_period) shiftsOnD = shifts.where(overlaps(shift, D)) shiftsInPeriod = shifts.where(overlaps(shift, period)) vehicles = allVehicles.map(v): s = find(shiftsOnD, assigned_vehicle_id == v.id) if s exists or v.status == 'maintenance': status = 'busy' busy_shift = s else status = 'available' return { ... } agents = allAgents.enabledOnly().map(a): s = find(shiftsOnD, assigned_agent_id == a.id) hours = sumHours(shiftsInPeriod, assigned_agent_id == a.id) if s exists: status='busy'; eligible=false else: eligible = ccnlRules.isEligible(a.id, proposedShiftForD?, D).eligible status='available' if eligible else 'available' (ma con ccnl_eligible=false) overtime = max(0, hours - settings.ccnl.overtime_threshold_week) return { id:a.id, name, status, ccnl_eligible:eligible, hours_period:hours, overtime_hours:overtime, busy_shift:s, ccnl_notes:... } sort vehicles: available first, then by (busy_shift.end_at asc) sort agents: 1) available & ccnl_eligible first 2) by hours_period asc 3) by overtime_hours asc 4) by last_name asc return { date:D, vehicles, agents } ``` > `proposedShiftForD?`: se l’assegnazione è per uno slot predefinito quel giorno (es. 08–16), passarlo per validazione live; se non c’è, `isEligible` valuta solo i vincoli “statici” (riposo/limiti alla data). --- ## Casi limite da coprire (test) * Mezzo in **manutenzione** → sempre **Non disponibile** con label “Manutenzione”. * Agente con turno che **sconfina** su D (es. notte 22:00–06:00) → è **Non disponibile** in D. * Overlap di turni → `POST /assign` deve restituire `422` con errore “overlap”. * CCNL: riposo < 11h tra fine turno precedente e inizio proposto → `eligible=false` + motivo. * Periodo mensile vs settimanale: calcolo ore coerente al setting. * Ordinamento agenti: a pari ore e overtime, ordinamento alfabetico stabile. * Fuso orario: usare TZ server/tenant, no ambiguità su DST. --- ## Accettazione (DoD) * Selettore data funziona; ricarica lista con calcoli corretti. * Colonne mezzi/agentI con **badge stato** e **CTA “Assegna servizio”** operative. * Ordinamenti rispettati come da regole. * API `GET /planning` e `POST /assign` implementate con validazioni CCNL. * Copertura test unit ≥ 80% per `ccnlRules` e funzioni di calcolo ore/overlap. * Performance: risposta `GET /planning` ≤ 300 ms con dataset medio (500 agenti, 200 mezzi, 2k turni/sett). --- ## Esempi (mock) ### GET /planning?date=2025-10-17 ```json { "date": "2025-10-17", "vehicles": [ { "id": 12, "plate": "AB123CD", "status": "available", "busy_shift": null }, { "id": 7, "plate": "ZZ987YY", "status": "busy", "busy_shift": { "id": 331, "start_at":"2025-10-17T06:00:00Z","end_at":"2025-10-17T14:00:00Z","service_name":"Pattuglia 3"}} ], "agents": [ { "id": 9, "name": "Rossi Mario", "status":"available", "ccnl_eligible": true, "hours_period": 24, "overtime_hours": 0, "busy_shift": null, "ccnl_notes": ["rest ok", "within weekly cap"] }, { "id": 4, "name": "Bianchi Luca", "status":"available", "ccnl_eligible": false, "hours_period": 38, "overtime_hours": 0, "busy_shift": null, "ccnl_notes": ["rest < 11h"] }, { "id": 2, "name": "Verdi Anna", "status":"busy", "ccnl_eligible": false, "hours_period": 32, "overtime_hours": 0, "busy_shift": { "id": 329, "start_at":"2025-10-17T08:00:00Z","end_at":"2025-10-17T16:00:00Z","service_name":"Portierato Ospedale"}} ] } ``` --- ## Note di implementazione * **Timezone**: salvare in UTC, presentare in TZ tenant. Usare libreria affidabile per overlap con turni notturni. * **Configurazione CCNL** per cliente/ente: tabella `settings_ccnl(tenant_id, key, value)` + cache. * **Sicurezza**: endpoint protetti (ruolo operatore/dispatcher). * **Accessibilità**: badge con testo + icona, contrasto AA. * **Telemetry**: loggare motivi di `ineligible` per audit. --- Se vuoi, alla prossima iterazione posso trasformare questo brief in: * scheletro **API (Node/Express + Prisma)**, * componenti **React/Next** per la UI (grid 2 colonne, card, modal assegnazione), * modulo `ccnlRules` completo con test unit.