- On Hold : onCellDrop REFUSE d'assigner un job en attente d'un prérequis (notify), reste au panneau (≠ 🔒 visuel) - Hors quart publié : marqueur ⚠ dans la cellule libre (offShiftJobs/rawCellJobs lit occByTechDay brut) + badge « hors quart » dans la timeline ressource — surface les jobs assignés un jour sans quart - Deep-link : Planif gotoDispatch(tech) → /dispatch?tech=&date= ; DispatchPage lit route.query (goToDay(date+T12:00:00) anti-décalage tz + selectTechOnBoard) - #58 : bouton « Désaffecter + aviser le client » dans le dialogue d'unassign Dispatch → roster.notifyReschedule (désassigne serveur + SMS lien /book au mobile du Customer) - Doc docs/features/roster.md mise à jour (Fait récemment / TODO) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
7.7 KiB
Markdown
97 lines
7.7 KiB
Markdown
# 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).
|