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.
