VigilanzaTurni/attached_assets/Pasted-Perfetto-Marco-Ti-preparo-un-task-brief-chiaro-e-operativo-da-dare-a-Replit-o-ad-un-dev-cos-i-1760694893657_1760694893657.txt
marco370 42a60fd32f Add CCNL settings management and validation for shift planning
Implement API endpoints for managing CCNL settings and introduce a rules engine for validating shifts against these settings, enhancing compliance with labor regulations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e5565357-90e1-419f-b9a8-6ee8394636df
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/6d543d2c-20b9-4ea6-93fe-70fe9b1d9f80/e5565357-90e1-419f-b9a8-6ee8394636df/EEOXc3D
2025-10-17 10:07:50 +00:00

272 lines
10 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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. Unarea 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 dassegnazione (mezzo/agente → servizio/turno alla data).
5. Per gli **agenti**: lordinamento “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 (lundom) **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 lassegnazione 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 dellagente 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:0016: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:mmhh: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 lassegnazione è per uno slot predefinito quel giorno (es. 0816), 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:0006: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.