gigafibre-fsm/docs/features/legacy-dispatch-bridge.md
louispaulb 2c3d7e9814 Pont legacy : coords GPS fiables (delivery→SL→RQA→Mapbox) + routage routier réel (Mapbox Matrix)
Pont (legacy-dispatch-sync.js) :
- Import des coordonnées par job via cascade : table legacy `delivery` (point de service exact,
  JOIN ticket.delivery_id) > Service Location ERPNext > géocodage RQA > géocodage Mapbox.
  Validation bornes Québec (coord()). Couverture 153/172 (89%).
- Géocodage RQA corrigé : retrait du générique de voie (Rue/Rang/Chemin absent de
  odonyme_recompose_normal) + code postal non accolé au terme (sinon ilike ne matche jamais).
- Repli Mapbox geocoding pour rues trop récentes pour le RQA (MAPBOX_TOKEN).
- Backfill + UPGRADE : coords delivery écrasent des coords SL moins précises (jamais l'inverse).
- Comptabilité honnête : vérifie r.ok sur create/update (erp ne throw pas) → errors + error_samples.
- Verrou de sérialisation sync() : tick + runs manuels ne se chevauchent plus (frappe_pg).
- Subject tronqué à 140 (champ Data) → corrige CharacterLengthExceededError sur jobs sans SL.
- Observabilité : coord_src tally + error_samples dans le résumé.

Ops Planification (éditeur de journée) :
- travelBetween() consulte une matrice Mapbox Matrix chargée à l'ouverture (loadDayRoute) →
  temps de trajet ROUTIERS RÉELS ; réordonnancement instantané sans nouvelle requête.
  Repli haversine si Mapbox indispo. Indicateur 🚗 réel vs 📏 vol d'oiseau.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:43:34 -04:00

108 lines
7.6 KiB
Markdown
Raw 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.

# Pont legacy → Dispatch (osTicket → Dispatch Job) — Handoff dev
Tire **régulièrement** les tickets ouverts assignés au compte « Tech Targo » dans la
DB legacy (osTicket/MariaDB `gestionclient`) et crée/maj un **Dispatch Job** ERPNext
pour les répartir (grille Planification / tableau Dispatch).
## Pourquoi « Tech Targo » = staff id 3301
Dans le legacy, le travail terrain à dispatcher est assigné au compte générique
**« Tech Targo »** (`staff.id = 3301`, username `tech`) — c'est le `default_staff` des
départements techniciens (Installation/Réparation/Fibre). Filtre du pont :
`ticket.status='open' AND ticket.assign_to=3301`. (~70 tickets au démarrage.)
Override possible via `LEGACY_TARGO_STAFF_ID`.
## Surfaces
| Quoi | Où |
|---|---|
| Module | `services/targo-hub/lib/legacy-dispatch-sync.js` |
| Routage + scheduler | `services/targo-hub/server.js` (`/dispatch/legacy-sync`, `startSync()` au boot) |
| Champ idempotence | Custom Field `Dispatch Job.legacy_ticket_id` (`dispatch-app/frappe-setup/setup_dispatch_custom_fields.py`) |
| Conso côté UI | Pool « à assigner » du tableau Dispatch + panneau « Jobs à assigner » de la Planification |
## Mapping ticket legacy → Dispatch Job
| Dispatch Job | Source legacy | Notes |
|---|---|---|
| `legacy_ticket_id` | `ticket.id` | **clé d'idempotence** (lookup avant create) ; affiché dans le panneau détail |
| `legacy_dept` | `ticket_dept.name` | département granulaire → **coloriage des cartes « comme legacy »** |
| `ticket_id` | `'LEG-' + ticket.id` | nom lisible du DJ |
| `subject` | `ticket.subject` | + adresse ajoutée si pas de Service Location |
| `customer` | `Customer``legacy_account_id = ticket.account_id` | 61/70 matchés ; sinon laissé vide |
| `service_location` + `latitude`/`longitude` | `Service Location` du customer (ville qui matche) | → pin carte |
| `job_type` | `ticket.dept_id` → {Installation, Réparation, Retrait, Autre} | valeurs valides du Select |
| `scheduled_date` | `ticket.due_date` (epoch) | converti **America/Toronto** (anti-décalage UTC) |
| `start_time` | `ticket.due_time` | `HH:MM` tel quel · `am`→08:00 · `pm`→13:00 · `day`→aucune |
| `priority` | `ticket.priority` (1/2/≥3) | → low / medium / high |
| `duration_h` | défaut par type | Install 2h · Répar 1.5h · autre 1h (le legacy n'en a pas) |
| `status` | — | toujours `open` (pool ; PAS auto-assigné à un tech précis) |
## Comportement
- **Idempotent** : 1 ticket legacy = 1 Dispatch Job (clé `legacy_ticket_id`). Re-run ⇒ 0 doublon.
- **Ne clobbe PAS le répartiteur** : un DJ déjà assigné/déplacé n'est plus touché ; maj de `scheduled_date`
seulement tant qu'il est encore `open` + non assigné.
- **SÉQUENTIEL** (frappe_pg ne supporte pas la concurrence) — pas de `Promise.all`.
## Coloriage des cartes (comme legacy)
`jobColor()` (`apps/ops/src/composables/useHelpers.js` → `legacyDeptColor`) colore par `legacyDept` :
Installation(Fibre)/Monteur/Fusionneur → **vert** `#46992f` · Réparation(Fibre) → **or** `#f1c84b` ·
Télé (install/réparation) → **rose** `#ec5fb0` · Téléphonie → **vert pâle** `#8fce93` ·
Désinstallation → **rouge foncé** `#c0392b`. (Tech en pause → rouge vif, priorité.) Le store
(`_mapJob`) expose `legacyDept`/`legacyTicketId` ; le pool « non-assigné » est déjà trié par `scheduledDate`.
## Endpoints
- `GET /dispatch/legacy-sync/preview`**dry-run, 0 écriture** : ce qui serait créé + matching client/SL + non-matchés.
- `POST /dispatch/legacy-sync/run` — exécute la synchro (création/maj). Retourne `{tickets, created, updated, skipped, errors, unmatched_customer}`.
## Récurrence
`startSync()` (server.js, au boot) — **opt-in** via env :
```
LEGACY_DISPATCH_SYNC=on # active la récurrence (sinon preview/run manuels seulement)
LEGACY_DISPATCH_SYNC_MIN=15 # période en minutes (défaut 15)
```
Posé dans `/opt/targo-hub/.env`. ⚠️ Après modif de `.env`, **recréer** le conteneur
(`cd /opt/targo-hub && docker compose up -d`) — `docker restart` ne relit pas l'env_file.
1er passage différé de 90 s après le boot, puis toutes les `MIN` minutes.
## État (mise en service 2026-06-06)
70 tickets importés (0 erreur, 9 clients non matchés = comptes post-migration + 2 tickets internes
« FORMATION EN HAUTEUR »). Récurrence active (15 min).
## Coordonnées GPS & routage routier réel (2026-06-06 f)
Le pont importe des **coordonnées fiables** par job (pour le routage routier réel dans l'éditeur de
tournée). Cascade de sources, de la plus précise à la plus large :
1. **`delivery` legacy** (point de service exact, via `ticket.delivery_id → delivery.latitude/longitude`)
— JOIN ajouté à `fetchTargoTickets`. Source de référence ; on préfère aussi l'**adresse de service**
(`delivery.address1/city/zip`) à l'adresse de facturation du compte.
2. **Service Location ERPNext** (coords du client matché) — repli.
3. **Géocodage RQA** (`address-search`/`address-validate`, Répertoire des adresses du Québec) — autoritaire
en rural. ⚠️ Le générique de voie (« Rue »/« Rang »/« Chemin ») est **retiré** du terme (absent de
`odonyme_recompose_normal` → sinon l'ilike ne matche jamais) et le **code postal n'est PAS accolé** au
terme (ses tokens seraient exigés dans le nom de rue).
4. **Géocodage Mapbox** (`MAPBOX_TOKEN`, clé publique) — couvre les rues trop récentes pour le RQA.
Contraint au Québec (`country=ca` + proximity + bornes `coord()`).
Validation `coord()` : bornes Québec (lat 44→63, lon 80→57) → rejette 0/0 et placeholders. Backfill
**+ UPGRADE** : sur un job existant, on remplit les coords absentes ET on **écrase** des coords Service
Location moins précises par les coords `delivery` (point exact) — jamais l'inverse. Caches géocodage au
niveau module (1 appel par adresse / vie du hub ; échecs mémorisés). Couverture mise en service :
**153/172 jobs (89 %)**`coord_src` : delivery 26 · SL 38 · RQA 17 · Mapbox 29 · aucune 15.
**Routage routier réel (Ops → Planification → éditeur de journée)** : `loadDayRoute()` appelle l'**API
Mapbox Matrix** une fois à l'ouverture (toutes les durées routières d'un coup) → `travelBetween()` retourne
le temps RÉEL ; le réordonnancement réutilise la matrice **sans nouvelle requête**. Repli haversine
(40 km/h) si Mapbox indispo. Indicateur 🚗 (réel) vs 📏 (vol d'oiseau).
## Robustesse (2026-06-06 f)
- **Comptabilité honnête** : `erp.create/update` ne *throw* pas (renvoient `{ok:false,error}`) → le pont
vérifie `r.ok` (sinon `errors++` + `error_samples` dans le résumé). Avant : creates échoués comptés réussis.
- **Verrou de sérialisation** sur `sync()` : tick récurrent + runs manuels ne se chevauchent JAMAIS
(frappe_pg sans concurrence → sinon « socket hang up » + écritures perdues dans un rollback).
- **Subject tronqué à 140** (champ `Data` Frappe) : les jobs sans Service Location ajoutaient l'adresse au
sujet → `CharacterLengthExceededError`. Détail complet conservé dans `legacy_detail`/coords.
- Env : `MAPBOX_TOKEN` ajouté à `/opt/targo-hub/.env` (clé publique `pk.`) → recréer le conteneur.
## TODO / idées
- Matcher les clients non matchés (créer le Customer / enrichir `legacy_account_id`) → réduit les 15 « aucune coord ».
- Géoloc live du tech (Capacitor `transistorsoft/capacitor-background-geolocation`) → 1er point de la tournée.
- Filtrer les départements non-terrain (ToDo, Support Informatique, Conception…) si bruit.
- Écrire en retour le tech assigné / la date vers le legacy (bidirectionnel) — non fait (lecture seule legacy).