# Planification (Roster AI) & assignation dispatch — Handoff dev

Grille hebdomadaire **ressources × jours** pour planifier les quarts, la garde, et
**prendre en charge les jobs dispatch** (glisser-déposer, timeline, occupation).
Aucune paie : on planifie, on approuve, on assigne.

## Surfaces

| Couche | Fichier | Rôle |
|---|---|---|
| UI Ops | `apps/ops/src/pages/PlanificationPage.vue` | Grille, garde, panneau « jobs à assigner », timeline ressource, dialogues d'impact |
| Client API | `apps/ops/src/api/roster.js` | Wrappers `fetch` vers `targo-hub /roster/*` |
| Backend | `services/targo-hub/lib/roster.js` | Endpoints, accès ERPNext, moteur de créneaux, orchestration solveur |
| Copilote | `services/targo-hub/lib/roster-assistant.js` | Actions d'écriture en langage naturel (réassign / SMS / escalade) |
| Solveur | `services/roster-solver/` (conteneur OR-Tools, réseau `erpnext_erpnext:8090`) | Propose un horaire (ne publie jamais) |
| RDV client | `apps/ops/src/pages/RendezVousPage.vue`, portail Lovable | Prise de rendez-vous (booking) roster-aware |

## Doctypes ERPNext (site facturation `erp.gigafibre.ca`)

- **Dispatch Technician** — ressources. Custom fields clés (cf. `dispatch-app/frappe-setup/setup_dispatch_custom_fields.py`) :
  `efficiency` (cadence globale), `skills` (CSV), `skill_levels` (JSON `{compétence:1-5}` = maîtrise),
  `skill_eff` (JSON `{compétence:facteur}` = vitesse par compétence), `cost_salary_h`/`cost_charges_pct`/`cost_other_h`.
- **Shift Template** — quarts. `on_call` (garde : capacité de réserve, **jamais** offerte au booking).
- **Shift Requirement** — besoins de couverture (« dispo vs requis »).
- **Shift Assignment** — assignations (statut Proposé/Publié). Le solveur propose, `/publish` écrit.
- **Tech Availability** — congés/absences (Approuvé). `long_term` = à remplacer (≠ vacances ponctuelles).
- **Dispatch Job** — les tickets. Champs utilisés ici : `assigned_tech`, `scheduled_date`, `start_time`,
  `duration_h`, `priority`, `parent_job`/`depends_on`/`step_order` (groupes), `booking_*`, `required_skill`.

## Concepts UI (PlanificationPage.vue — voir la carte des sections en tête de `<script setup>`)

- **Garde LIVE** : jamais matérialisée. `gardeOverlay` (règles de rotation) ⊕ `manualGarde` (touche **G**, localStorage)
  → `gardeEffective`. Recalculée à chaque rendu ⇒ pas de désync. La garde est **exclue** des heures travaillées et du coût.
- **Filtre compétences (ET)** : `skillFilter` — un tech n'apparaît que s'il a **toutes** les compétences cochées.
- **Score de priorité** : `priorityScores` = maîtrise (`techCompetence` 1-5) ⊕ vitesse (`techSpeed`, efficacité par
  compétence) ⊕ coût. Hook **proximité** (`techProximity`) câblé mais **neutre** (renvoie `null`) — secteur géré
  manuellement via les zones ; réservé à 20 % du score quand la géoloc arrivera.
- **Occupation** : `occCells` combine la fenêtre de shift (`bookableH`) et les jobs (`occByTechDay` du hub) →
  `usedH`, `pct`, `blocks` (barres colorées), `jobs` (liste triée priorité→heure). ⚠️ **un bloc n'apparaît que si le job
  a un `start_time`** (sinon il compte dans `usedH` mais reste invisible — d'où le premier-trou-libre, voir hub).
- **Timeline ressource** : `openTimeline(t)` → dialogue listant les jobs de la semaine visible (barre + liste priorisée).
  0 appel réseau (réutilise les helpers de cellule). Lien « Dispatch » vers le tableau (`/dispatch`).
- **Panneau « jobs à assigner »** (Outils) : flottant, déplaçable. Multi-sélection (cases), groupes parent-enfant
  surlignés, heuristique **terrain vs à distance** (`jobIsOnsite` : activation/config/netadmin = à distance → pré-décoché),
  pré-total d'heures, **glisser la sélection** sur une case → assignation **séquentielle**, **aperçu d'occupation projetée**
  (barre fantôme + badge `+Xh → Y%`) au survol.
- **Dialogues d'impact** : retirer une compétence ou poser une absence sur un tech avec jobs assignés → liste des jobs
  affectés + **candidats classés** (qualifiés + libres au créneau) ou « À recontacter ».

## Endpoints hub `/roster/*` (extrait)

| Méthode | Chemin | Usage |
|---|---|---|
| GET | `/technicians` `/templates` `/requirements` `/assignments` `/coverage` `/stats` `/occupancy` `/absences` | Lecture grille |
| POST | `/generate` | Solveur OR-Tools (propose) |
| POST | `/publish-week` | Écrit les Shift Assignment (diff : crée/retire seulement le delta) |
| POST | `/technician/:id/skills` `/pause` `/efficiency` `/cost` | Édition ressource |
| GET | `/unassigned-jobs` | Jobs ouverts/On Hold sans tech (+ groupe/dépendances) pour le panneau |
| POST | `/assign-job` | Assigne un job (pose `start_time` **premier-trou-libre** + garantit `duration_h`) |
| POST | `/backfill-start-times` | Pose un `start_time` sur les jobs **déjà** assignés sans heure (idempotent) |
| GET | `/skill-impact` `/absence-impact` `/job-candidates` | Données des dialogues d'impact |
| POST | `/skill-impact/redistribute` | `plan` (explicite) / `auto` / `requeue` |
| GET/POST | `/book/slots` `/book/fit` `/book/confirm` `/book/hold` `/book/meta` `/policy` | Prise de RDV (booking) |

Moteur de créneaux partagé : `loadBookingData` → `techGaps` (trous libres) → `firstFitStart` (premier trou pour une durée,
réutilisé par `assign-job` **et** `backfill-start-times`) → `bookingSlots` / `fitBooking`.

## Pièges (à NE PAS réintroduire)

- **frappe_pg ne supporte PAS la concurrence** : `erp.list`/écritures en **séquentiel**, jamais `Promise.all`
  (sous charge concurrente la connexion PG se réinitialise → « load fail »). Les sauvegardes d'édition sont **debouncées**.
- **Occupation invisible sans `start_time`** : un job assigné sans heure ne dessine aucun bloc. `assign-job` pose donc
  toujours une heure (premier-trou-libre). Le backfill rattrape l'historique. Si le tech n'a **aucun quart** ce jour-là,
  on ne peut pas placer de bloc (écart de planification, pas un bug).
- **Scheduler ERPNext en pause** : la facturation legacy reste autoritaire jusqu'au cutover.
- **Déploiement** : front via `apps/ops/deploy.sh` (`DEPLOY_BASE=/ops/ quasar build` + tar/scp → `/opt/ops-app/`) ;
  hub via `scp lib/roster.js → /opt/targo-hub/lib/` + `docker restart targo-hub`. Custom fields via le script frappe-setup.

## Fait récemment

- ✅ **On Hold** : le dépôt d'un job en attente d'un prérequis est REFUSÉ (notify), plus seulement 🔒 visuel (`onCellDrop`).
- ✅ **Alerte hors quart** : un job assigné un jour où le tech n'a aucun quart publié → marqueur ⚠ dans la cellule libre
  (`offShiftJobs`) + badge « hors quart publié » dans la timeline. (Le backfill ignore ces jobs : pas de bande à remplir.)
- ✅ **Deep-link Dispatch** : Planification → `gotoDispatch(tech)` ouvre `/dispatch?tech=&date=` ; DispatchPage lit `route.query`
  (`goToDay(date+'T12:00:00')` anti-décalage tz + `selectTechOnBoard`).
- ✅ **#58 Aviser le client** : bouton « Désaffecter + aviser le client » dans le dialogue d'unassign Dispatch →
  `roster.notifyReschedule` (désassigne serveur + SMS lien /book au mobile du Customer).

## TODO / dette

- Brancher la **proximité** (lat/lng Service Location ↔ base/secteur du tech) dans `techProximity` (hook neutre en place).
- Apprentissage IA compétence ↔ type de cas (historique des jobs).
- `#57` Hold côté client sur `/book` (Lovable) : bloquer la plage à la sélection.
- Compteur global « N jobs hors quart cette semaine » dans la barre d'outils (complément du marqueur ⚠).
- **Refactor** : `PlanificationPage.vue` (~1,5k lignes) gagnerait à extraire des composables
  (`useGarde`, `useOccupancy`, `useAssignPanel`, `useSkillEditor`) — non fait (risque sur fichier prod).
