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
272 lines
10 KiB
Plaintext
272 lines
10 KiB
Plaintext
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.
|