Commit Graph

31 Commits

Author SHA1 Message Date
louispaulb
48faeba72b fix(dispatch): coords job EXACTES (fibre par service) + hardening distances auto-dispatch
Précision coords (le levier amont unique : alimente géofence + matrice OSRM + solveur, tous lus à chaud) :
- fibreByDelivery(deliveryId) : AUTORITÉ #0 = le DROP d'installation exact via ticket.delivery_id → service → fibre.*_service_id
  (placemark d'infra si lié, sinon coord fibre). Câblé dans buildJob (avant resolveDevCoords) ET geolocateJobs (refreshFibre,
  seuil de réécriture 30 m car drop exact). Le tick horaire l'applique tout seul désormais.
- BACKFILL exécuté : 15 jobs actifs corrigés (14 fibre_service_placemarks) — dont LEG-247012 (7,5 km !) et LEG-254147 (14 km) qui
  étaient grossièrement mal géocodés → snappés au drop exact. Idempotent (re-run 0).

Hardening auto-dispatch (audit) :
- osrmMatrix : un nœud SANS coords n'est plus à 0 min de tout (« adjacent gratuit » → insertion n'importe où) mais à 180 min
  (« loin/inconnu ») → le solveur ne chaîne plus gratuitement les jobs sans coords au milieu des vrais arrêts.

Restent des jobs sans source coord (ex. lots camping Lac des Pins sans ligne fibre) = data gap F, pas résolvable ici.
Suivi recommandé (non fait, flag) : valider bornes QC sur les chemins de LECTURE (pas que l'ingestion) ; retirer les défauts
tech silencieux (Montréal/Ste-Clotilde) ; sortir les jobs sans coords du solveur vers le pool « à situer » ; invalider le cache
matrice SPA quand les coords changent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:36:08 -04:00
louispaulb
cd25fa2504 fix(dispatch): double-assign F + médias terrain dans le ticket + deep links natifs
- capacityByDay compte les jobs assignés SANS heure (untimed_used_h au prorata AM/PM/Soir)
- /roster/assign-job : garde 409 si le ticket F est déjà à un autre vrai tech (force pour écraser) ; SPA = dialog confirm
- mirrorAssign : start_time depuis F due_time (am/pm) + fix due_date (cur.→r.) + backfill fill-only dans watchLegacy (97% des tickets F = 'day' → le spread capacité est le fix porteur)
- pushAssignments : garde anti-clobber (conflits F skippés, force=1) + compteur conflicts
- ticket-collab : courriels de notification → deep link OPS /#/tickets?open= (fini le desk) ; TicketsPage lit ?open= ; Dashboard ouvre le ticket précis
- DetailModal + SupplierInvoices : lien desk gaté can('manage_settings')
- NOUVEAU JobMediaModule (photos tech géo-vérifiées + journal terrain) monté dans IssueDetail onsite + volet Planif ; hub /roster/job-media + /field/photo-file signé ; JobThread poll silencieux 45s
- legacy-sync emailList : strip caractères invisibles (soft hyphen U+00AD…) — fixait un 417 InvalidEmailAddress en boucle (compte 15988)

Déployé prod 2026-07-18 (hub restart + SPA /opt/ops-app), sondes live OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 18:02:37 -04:00
louispaulb
9c1b578d5d feat(dispatch): provisioning identites enrichi Employee+Authentik (multi-courriels)
extEmailIndex(): index nom->emails depuis ERPNext Employee (company/prefered/
personal/user_id) + Authentik /core/users. provisionIdentities l utilise pour:
- CREATE des identites d ex-staff sans courriel legacy (si une source externe en a un)
- ENRICH: tous les courriels decouverts s accumulent en alias_emails (union,
  jamais overwrite) -> multi-courriels par personne, resolvable par chaque alias
- skip des comptes systeme (ticket dispatch, Gestion Inventaire, Tech Targo)
Applique en prod: +1 lien, +5 identites enrichies d alias (perso+corpo). Idempotent
(re-run 0 ecriture). Les 41 restants = ex-employes absents de toute source ->
mapping manuel via l onglet Identites (liste unresolved du rapport).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:50:23 -04:00
louispaulb
e98b21ba0a feat(dispatch): provisioning identites depuis legacy staff (convergence ledger)
provisionIdentities({dryRun,scope}) : pour chaque staff du ledger, garantit une
identite canonique portant legacy_staff_id (+ tech_id si Dispatch Technician, donc
les admins restent non-tech). LINK (identite existante par courriel/nom -> remplit
legacy_staff_id) ou CREATE (depuis staff si courriel). Idempotent. Route
GET|POST /dispatch/legacy-sync/tech-history/provision-identities?scope=ledger|active.
Applique en prod: +33 identites liees a leur legacy_staff_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:23:23 -04:00
louispaulb
60da7a01da feat(dispatch): tech-history resolue via identite canonique (dynamique)
Le rapport techHistory groupe desormais par IDENTITE (lib/identity resolveIdentity:
staff_id -> nom), calculee A LA LECTURE -> corriger une identite (onglet Settings
Identites) reclasse le ledger AUSSITOT (pas de valeur figee).
- is_tech = l identite a un tech_id : le staff admin (ex. Megane Liaud, sans tech_id)
  n est PLUS compte comme technicien.
- Sortie: par assignee {label,email,is_tech,kind,total,from_pool} + liste unresolved
  (noms sans identite, ex. prenom seul) a mapper dynamiquement.
- Detail ?tech= accepte cle identite | courriel | tech_id | staff_id | nom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:14:30 -04:00
louispaulb
6761c9e05a feat(dispatch): ledger historique d assignation par tech (ops_tech_ticket_history)
Reconstruit la timeline complete d assignation des tickets depuis le CHANGE LOG
osTicket (ticket_msg: 'assign... change de X a Y') vers une table plate dans la
PG ERPNext (a cote de tabDispatch Job/tabIssue), incluant les tickets fermes.

- techHistoryBackfill(): scan watermarke borne de ticket_msg (I/O previsible),
  parse + mapping nom->staff->TECH-<id>, upsert idempotent. Backfill initial + tick
  incremental (un seul chemin) -> capte les nouveaux changements (les notres ET ceux de F).
- techHistory({tech}): rapport par tech (detail) ou recap tous techs.
- Routes: GET /dispatch/legacy-sync/tech-history[?tech=], GET|POST .../tech-history/backfill.
- Tick live planifie dans startSync (LEGACY_TECH_HISTORY, defaut on; LEGACY_TECH_HISTORY_MIN=15).
- Detecte 'de Tech Targo a <tech>' = sortie du pool vers un tech (from_pool).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 11:58:03 -04:00
louispaulb
e43f04d3e5 feat(dispatch): write-back unifie Publier vers F + tickets perimes natifs
Hub (legacy-dispatch-sync.js):
- publishPreview() + GET /dispatch/legacy-sync/publish-preview: apercu unifie
  OPS->F (assignation + fermeture + horaire) + avertissements tech non mappe
  = la liste des changements avant du bouton. Lecture seule.
- publishJob() + GET|POST /dispatch/legacy-sync/publish-job: apply par job
  (GET=plan dry-run, POST=applique) via ops_reassign.php.
- staleTickets() + GET /dispatch/legacy-sync/stale: tickets ouverts sans
  activite >=N j (natif, remplace le courriel stale de F); scope buckets/maxAge en SQL.
- staleNudge() + GET|POST /stale-nudge + digest planifie opt-in
  (LEGACY_STALE_NUDGE), anti-spam persiste, nudge via alertWebhook.

Bridge F (ops_reassign.php):
- action schedule: write-back due_date/due_time (horaire OPS->F).

Verifie en prod (lecture seule + dry-run; aucune ecriture F reelle tiree).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:49:28 -04:00
louispaulb
956e96b8bc chore(hub): sync repo with deployed hub state (uncommitted prod work)
Le hub deploye (/opt/targo-hub) avait derive du depot (deploiements scp
directs non commites). Rapproche la branche de la realite deployee AVANT
d empiler le nouveau travail:
- legacy-dispatch-sync.js: liens natifs source_issue/depends_on/parent_incident
  + backfills (deployes, jamais commites)
- ops_reassign.php: action post (note/reponse au fil, deployee)

Aucun secret (creds via ops_secret.php). Fichiers = copie exacte du deploye.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:49:17 -04:00
louispaulb
14530787bb consolidate uncommitted multi-session work (NL staff-agent, user avatars/profil, HelpHint, ticket detail modules, skill icons, hub agent/payments/sync)
Lot de travaux accumulés non commités (plusieurs sessions), vérifiés : hub `node --check`
tout OK + build SPA propre.
- ops : AssignmentField, CommandPalette, IssueDetail/DetailModal + detail-sections/modules,
  UserAvatar/useAvatar, UserProfileDialog/EmployeeEditDialog, HelpHint, useSkillIcons,
  pages (Dashboard/Equipe/Clients/Tickets/Reports/Settings/Evaluations/LegacySync…),
  usePermissions/useUserGroups/useDetailModal, MainLayout, TicketStatusControl
- hub : staff-agent.js (couche commandes NL), agent.js/agent-tools.json/voice-agent.js,
  avatars.js, conversation.js, payments.js, sync-orchestrator.js, auth.js, campaigns.js,
  legacy-dispatch-sync.js, server.js

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:43:50 -04:00
louispaulb
4870cec09b hub+solveur : compétences multi par job + réservation + matérialisation quarts + app terrain natif
- roster.js : store /roster/job-skills (LISTE = source unique des compétences requises),
  enrichissement required_skills[] + required_skill (1re, rétro-compat), solveur techCovers
  (le tech doit les avoir TOUTES) ; /roster/reserve (bloc placeholder priorité moyenne, soft) ;
  publishWeek modes draft/submit/approve/publish ; matérialiseur de quarts récurrents
  (source:pattern) ; décodage des entités HTML aux points de service
- route_solver.py : skill_ok list-aware (toutes les compétences requises)
- dispatch.js : suggestSlots ignoreReserved (une réservation ne bloque pas la recherche de créneau)
- conversation.js : agentName sur le fil (commentaire au nom du tech, pas « Targo Ops »)
- legacy-dispatch-sync.js : postTicketLegacy(actorEmail, actorName) — commentaire attribué
- field-app.html + server.js : commentaire + fil de ticket NATIFS (restent sur OPS)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:46:42 -04:00
louispaulb
e950bd3ac6 hub: reverse geocoding infra-first (fibre field-pins -> RQA fallback); address resolution + geotagged field photos
- /address/reverse: reverseNearestFibre() (fibre field-pins, source of truth) then RQA
- searchCustomers: civic address resolves to the resident (not fuzzy homonyms)
- field-app + /field/photo: geotagged photos confirm the correct job

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:52:14 -04:00
louispaulb
0e5c890b74 perf(hub): cache liste techs (30s) + garde anti-chevauchement sync + jeton ingest mémoïsé
Batch 2 de l'audit (perf backend), items sûrs :
- roster.js : fetchTechnicians caché 30 s (appelé par endpoint + generate + materialize +
  tech-positions polling 25 s pour des données quasi statiques). invalidateTechCache() sur
  les 6 écritures Dispatch Technician (coût/effic/skills/pause/traccar/weekly) → pas de stale
  après édition. Vérifié : call1 71 ms → call2 5 ms (caché).
- legacy-dispatch-sync.js : garde _inFlight sur le tick d'import ET la veille → un passage
  long ne se relance plus par-dessus (évitait doublons Dispatch Job + contention ERPNext).
- conversation.js : jeton d'ingestion webhook mémoïsé (mailIngestToken) au lieu d'un
  fs.readFileSync SYNCHRONE à chaque courriel/canal entrant (bloquait la boucle d'événements).

NON fait (par prudence) : paralléliser les erp.list séquentiels de generate() (séquencement
DÉLIBÉRÉ anti-ECONNRESET Frappe, cf. feedback_hub_crash_502) ; batch N+1 Customer du triage
(refactor risqué d'une chaîne de fallback stateful — reporté) ; jitter des pollers (502 boot
déjà mitigé).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:42:18 -04:00
louispaulb
a7a6baf8b5 feat(geoloc): OSM geocoder + postal-code centroid fallback (never wrong-city)
The ingest geocode fallback called Mapbox with only a broad territory
bbox guard — so a wrong-city guess (e.g. "Rue Elsie" → Valleyfield, both
in-territory) was accepted. Rebuilt the chain to be bounded:

  infra (fibre/placemarks) → RQA exact → OSM (validated) → postal-code
  centroid → phone-match / town-centre (existing last resorts)

- geocodeNominatim(): free OpenStreetMap geocoder (User-Agent per OSM
  policy, cached, in-territory) — replaces Mapbox in the ingest path.
- OSM result is ACCEPTED only if within 10 km of the postal-code centroid
  (geocodeCentroid, RQA average for the full postal code); otherwise
  rejected → the centroid is used. So a J0S1H0 address can never land in
  Valleyfield again.
- postal_centroid is the guaranteed bounded fallback.

Note on connectors: the fibre table is a PERMANENT direct MySQL link
(cfg.LEGACY_DB); placemarks come via the permanent PHP bridge
ops_placemarks.php — both already wired into resolveDevCoords.

Verified: J0S1H0 centroid = 45.0857,-74.1795 (Huntingdon, 3471 addrs);
OSM reachable and returns in-town coords for known streets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 08:28:10 -04:00
louispaulb
e9d1990235 fix(geoloc): trust infrastructure (fibre) for exact address; fix new-street mislocation
Bug: "102-4 Rue Elsie, Huntingdon J0S1H0" (a NEW street, absent from RQA)
showed in Valleyfield. The F `fibre` table had the correct placemark
(45.091152,-74.180855) but the job was stuck at 45.264,-74.144.

Root causes:
1. _parseAddr extracted civic "102" from "102-4" → fibreMatch queried
   `terrain LIKE '102%'`, matching MANY other streets (Lost Nation,
   Dalhousie, Ridge…) and burying/missing the Elsie `terrain '102-4'`.
   Now captures the full "NNN-N" civic → `terrain LIKE '102-4%'` (precise)
   and cleans the street ("Rue Elsie", no "-4" leftover).
2. resolveDevCoords let the (stale/wrong, account-fallback) delivery
   coords win before fibreMatch. Now a STREET-CONFIRMED fibre match is
   the top authority (_streetConfirmed) — "se fier à la base infra".
3. Existing wrong jobs never self-corrected (sync only fills missing
   coords). Added geolocateJobs refreshFibre mode + ?refreshFibre=1:
   re-checks already-coordinated jobs, overwrites ONLY when fibre
   confirms the street AND it moved >100m.

Ran the correction: 11 jobs fixed (all fibre placemarks), incl.
LEG-253136 moved 19 km to Huntingdon. Verified read-back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 07:57:22 -04:00
louispaulb
512c4a5f1b feat(ops): dispatch auto complet + perf Boîte/rapports + fix session
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).

Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00
louispaulb
0f65c02d83 feat(fsm): platform build — comms UI, F→ERPNext sync/billing, roster, campaigns, network, reports
Accumulated work on the dispatch/legacy-writeback branch:
- Communications UI: CommunicationsPage, ConversationFullPage, DepartmentBoard,
  PipelineBoard, ReaderStack, Orchestrator/NewTicket/ServiceStatus/Outbox dialogs;
  hub gmail.js, ticket-collab.js, outbox.js, coupon-triage.js, client-diag.js.
- Billing/sync mirror (F→ERPNext): legacy-payments.js, legacy-sync.js,
  sync-orchestrator.js, supplier-invoices.js, municipality.js + incremental
  migration scripts; LegacySyncPage, SupplierInvoices + negative-billing /
  terminated-active reports.
- Roster/campaigns/network/voice: roster + roster-assistant, campaigns, giftbit,
  olt-snmp, traccar, twilio, vision, tech-absence-sms, ai/agent/config/helpers,
  legacy-dispatch-sync; ops PlanificationPage, RapportsPage, Settings, Tickets,
  ClientDetail updates.
- docs/ PLATFORM_GUIDE + UI_AND_OPTIMIZATION; .gitignore __pycache__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:12:12 -04:00
louispaulb
98458861c3 feat(dispatch): capacité AM/PM, durées additives, capture terrain (/field), sync techs
Hub (lib/roster.js, vision.js, legacy-dispatch-sync.js, server.js) + Ops + pont legacy.

- Capacité par jour AM/PM (Soir = réserve garde/urgence, jamais offerte) calculée
  client-side sur les techs visibles -> suit le filtre de compétence.
- Modèle de durée ADDITIF (caractéristiques, tableur inline) + auto-détection
  DÉTERMINISTE par mots-clés (sans IA permanente) ; est_min branché sur capacité + pool.
- Capture terrain passive : endpoints publics /field (job/tech/checkpoint/ts/photo/
  device/vision), tokens HMAC signés sans PII ; dérive actual_start/end. UI hébergée
  public/field-app.html (liste/carte Mapbox/Street View/photo/scan MLKit->Gemini).
- Chrono job (start/finish), repositionnement carte (set-location), vue satellite,
  Street View clic-droit, année devant les dates dues groupées.
- Sync techniciens : rapport de réconciliation 3 systèmes (staff legacy / Dispatch
  Technician / groupe Authentik), application MANUELLE, zéro écriture Authentik (+11 fiches).
- vision.js : extractEquipment() réutilisable (marque/modèle/série/MAC/codes-barres).
- Pont legacy (ops_reassign.php) : désassignation reflétée, fermeture ticket, retour
  au pool ; notification courriel à l'assignation.

Déployé sur le hub ; ce commit aligne le repo sur l'état en production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:47:21 -04:00
louispaulb
3d65b994e5 feat(dispatch): fermer ticket (unitaire + lot), tri ASC/DESC, batch PHP
- PHP ops_reassign.php : action `close` (single) + `batch_close` (N tickets en 1 connexion → efficace)
  ; close = status=closed + date_closed + closed_by=acteur Authentik + réouverture enfants waiting_for + log.
- Hub : closeTicketLegacy + batchCloseLegacy + routes POST close-ticket / batch-close (marque Dispatch Job Completed).
- Ops : bouton « Fermer ce ticket » (fil du panneau) ; « Fermer (N) » sur la sélection (fermeture en lot) ;
  tri ASC/DESC du panneau (toggle, surtout pour la date).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:40:26 -04:00
louispaulb
4abce6fd66 feat(dispatch): pont d'écriture legacy + adresses/coords/carte/UX Planification
Pont d'écriture Ops → legacy osTicket via endpoint PHP token-gated (hérite des
droits write de l'app facturation.targo.ca → AUCUN grant DB). Fidèle à ticket_view.php :
réassignation (assign_to + participant[assistants] + followed_by + ticket_msg + lock
respecté) + fermeture (status=closed + date_closed + closed_by + réouverture des enfants).
Auteur du log = utilisateur Authentik réel (email → staff legacy par email/nom).

Hub (legacy-dispatch-sync.js):
- adresse de SERVICE importée (champ address) + reimportAddresses + fillMissingCoords
- garde TERRITOIRE (rejette les homonymes hors-zone Gaspésie/Outaouais/Estrie) + centroïde CP/ville
- purgeStaleOrphans (anciens imports TT- périmés), # ticket legacy dans titre + description
- legacyWrite (POST form + X-Ops-Token lu d'un fichier), pushAssignments (reassign), closeTicketLegacy
roster.js: occupancy + unassigned-jobs exposent address/latitude/longitude.

Ops (PlanificationPage.vue):
- carte des jobs à assigner (pins couleur=compétence + lettre repère liste↔carte), chips
  filtre par type, panneau ouvert par défaut
- clics cellule: bande quart/garde + blocs jobs → tournée ; fond → menu horaire (fini le clic droit)
- éditeur de tournée: itinéraire routier réel + adresse + fil ticket (expand), refresh occupation à l'assignation
- bouton « Publier au legacy » (aperçu + ✕ désassigner) + « Fermer le ticket »
- fix dates (lundi calculé en local — plus de décalage UTC) + nav ±1 jour

services/legacy-bridge/ops_reassign.php: endpoint (placeholders; secrets injectés au déploiement, hors repo).
scripts/migration: backfill_dispatch_address.sql + diagnostics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:32:00 -04:00
louispaulb
1673f7cd9a Pont : import basé sur l'ADRESSE DE SERVICE (delivery), pas la facturation (compte)
Problème de fond : quand ticket.delivery_id ne se joignait pas, le pont retombait sur l'adresse de
FACTURATION du compte (résidence) → sujet + géocodage faux. Or les 124 tickets ouverts-3301 ont tous un
delivery côté compte (98 via ticket.delivery_id, 26 via le compte).

- fetchTargoTickets : résolution delivery robuste — COALESCE(ticket.delivery_id, delivery du compte AVEC
  coords [plus récent], delivery du compte [plus récent]) → l'adresse de SERVICE est toujours disponible.
- buildJob préfère déjà svcAddr (delivery) au billAddr → sujet + géocodage utilisent le service.
- Rafraîchit le `subject` des jobs encore au pool (open + non assigné) pour refléter l'adresse de service
  (corrige les anciens sujets basés sur la facturation) ; ne touche pas un job déjà dispatché.

Résultat (re-sync) : delivery 26→37, no_coords 6→1, 0 erreur. Jobs ouverts affichent l'adresse de service
(« Camping Sandysun · 32 Bellevue », « Lac des pins · 25 Érable », « Franklin · 35 rue Hilltop »…).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:36:17 -04:00
louispaulb
de496f8405 Pont : géoloc camping (fixe) sur les Dispatch Jobs — l'adresse de service ≠ résidence du client
Symptôme : un job de camping (« Lac des pins | Anton Rimerov ») pointait sur la RÉSIDENCE du client
(428 Rue George, Lasalle = 45.58,-73.73) au lieu du camping. Le pont géocodait l'adresse de compte.

- buildJob : détection camping en PRIORITÉ MAX via le registre camping_registry — signal = sujet (label
  explicite, prioritaire) puis ville/adresse de delivery. Garde-fou : le texte doit contenir « camping » OU
  un mot-clé de LIEU spécifique (évite les faux positifs de patronyme, ex. « Daniel Dauphinais »). coord_src='camping'.
  La branche update fait écraser les coords existantes par le camping (comme delivery). 20 jobs ouverts re-coordonnés.
- camping_dispatch_backfill.sql : corrige les jobs DÉJÀ dispatchés (que le sync ne re-traite plus car le ticket
  legacy a quitté le pool ouvert-3301) → 4 Lac des Pins + 2 SandySun. Anton Rimerov/Germaine Thibert → 45.0624,-73.9113 ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:30:11 -04:00
louispaulb
fbf584f02f Phase 1 (hygiène) : utils partagés + logique pure testable + observabilité erp + 1ers tests
Modularisation / dé-duplication :
- lib/util/text.js : `norm` canonique partagé (remplace 2 ré-implémentations : address-db, legacy-dispatch-sync).
- lib/util/legacy-parse.js : parseurs/mapping PURS du pont (DEPT_JOBTYPE, DUR, jobType, prio, tzDate,
  startTime, coord) extraits hors I/O → testables en isolation, sans pg/mysql/erp.
- legacy-dispatch-sync + address-db importent ces utils (pont vérifié en prod : preview OK, 0 erreur).

Observabilité (sûr, additif, 1 seul point) :
- erp.js create/update/remove : log de l'échec à la SOURCE quand HTTP≥400 → toutes les écritures ERPNext
  silencieuses des 50+ appelants sont désormais tracées, SANS changer aucun flux de contrôle.

Tests (fondation) :
- vitest + npm test ; test/util.test.js : 19 tests verts sur norm + coord(bornes QC)/prio/startTime/jobType/tzDate.
  Tournent sans installer les deps lourdes du hub (modules purs).

Aligné docs/architecture/VISION.md (P0 hygiène). Suite : audit r.ok des appelants financiers (payments/contracts)
en revue supervisée ; CI/CD minimal (Gitea Actions lint+test) ; décomposition des god-files (Phase 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:36:41 -04:00
louispaulb
60dc095f56 Pont legacy : géocodage RQA via la recherche TRIGRAM (RPC search_addresses) + garde-fou anti-faux-positif
- address-search.js : expose searchAddressesRpc() → RPC Postgres `search_addresses` (pg_trgm), la MÊME
  recherche que l'autocomplete de disponibilité fibre. Trouve les rues que l'ilike manquait (générique géré
  par la colonne odonyme_recompose_long + phase 2 trigram), priorise les CP J0L/J0S (territoire).
- geocodeRQA() (bridge) bascule de l'ilike vers la RPC. Garde-fou : la phase 2 trigram dérive quand le
  civique est absent du RQA (« 2245 René-Vinet » → « Rue Grenet, Montréal »). On n'accepte un résultat que si
  le civique concorde + un token de nom de rue correspond + (territoire J0L/J0S OU CP/ville legacy concordants).
  Vérifié sur les données réelles : accepte 494 Av Curry / 3055 Routhier / 228 Principale / 61 Jean-François ;
  rejette René-Vinet→Grenet/Panet, chemin Ridge→Ferme, rue West→Perras (bons faux positifs écartés).
- Le faible compte RQA (8) = haute précision (l'ilike comptait 17 dont des faux positifs). Mapbox couvre le
  reste (rues neuves/civiques absents) ; ~109/125 (87 %) coordonnés ; les « aucune » = campings/villes mal écrites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:05:14 -04:00
louispaulb
e14026ae23 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
louispaulb
be222d005e feat(dispatch): date d'ouverture + MàJ en tête du détail ticket (mouseover) pour juger la fermeture
- pont : SELECT date_create+last_update ; legacy_detail préfixé « 🗓 Ouvert <date> · MàJ <date> » puis description.
  Backfill = réécrit legacy_detail si différent (idempotent). Visible dans le mouseover du panneau roster + le détail Dispatch.
  Ex. révélé : LEG-111325 ouvert 2021 jamais touché → candidat fermeture évident.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:07:15 -04:00
louispaulb
8707cd7c2e fix(dispatch): pont lisait un réplica figé (avril) + auto-fermeture des DJ dont le ticket legacy est closed
- ROOT CAUSE : LEGACY_DB_HOST='legacy-db' = réplica figé au 2026-04-07 → tickets fermés/réassignés depuis
  paraissaient encore ouverts (ex. 244659). Fix infra : .env LEGACY_DB_HOST=10.100.80.100 (DB live, SELECT-only).
- closeResolved() : tout DJ issu du pont (open/assigned/On Hold) dont le ticket legacy est 'closed' → status 'Completed'.
  Appelé à chaque sync + route POST /dispatch/legacy-sync/close-resolved. Résultat 1er run live : 125 ouverts réels,
  102 créés, 44 fermés (dont LEG-244659). NB In Progress non touché.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:03:10 -04:00
louispaulb
2631ed9199 feat(dispatch): couleur ticket = couleur skill + fil complet du ticket + tri pool (date/ville/priorité)
- Couleurs liées aux skills (éditable/cohérent) : hub deptToSkill() déduit une compétence du type legacy
  → /roster/unassigned-jobs renvoie required_skill ; PlanificationPage colore la carte par getTagColor(required_skill)
  (même couleur que le chip skill) ; bordure 5px
- Fil complet du ticket : hub /dispatch/legacy-sync/ticket-thread (ticket_msg + auteur staff, HTML nettoyé) ;
  api legacyTicketThread ; RightPanel bouton « 💬 Voir le fil / commentaires » (chargé au clic, messages+auteurs+dates)
- Order-by du pool dispatch : useBottomPanel.bottomSort (date|city|priority) + dropdown ⇅ dans BottomPanel
  (ville = 2e segment adresse / token sujet avant |)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:37:45 -04:00
louispaulb
b492e30971 feat(dispatch): réconciliation + heartbeat + détails ticket dans Ops + couleurs panneau roster
#1 « ne rien échapper » :
- /dispatch/legacy-sync/reconcile : compare legacy(3301,open) ↔ Dispatch Jobs → missing/orphan (70↔70, 0/0)
- /dispatch/legacy-sync/status : heartbeat (last_sync + stale) pour Uptime-Kuma

Détails ticket dans Ops (bug signalé) :
- pont extrait le 1er message du fil legacy (stripHtml) → champ legacy_detail (backfill 70)
- store legacyDetail ; RightPanel : section « Détails du ticket » ; tooltip dans le panneau roster

Couleurs panneau roster (bug signalé) :
- /roster/unassigned-jobs renvoie job_type/legacy_dept/priority/legacy_* ; PlanificationPage colore
  chaque carte par type (legacyDeptColor partagé) — bordure gauche

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:50:21 -04:00
louispaulb
7f22647ce4 feat(dispatch): bouton « Activer STB (Ministra) » — lien d'activation TV extrait du ticket legacy (Phase 1)
- pont : extrait le lien connect_ministra.php du ticket_msg (déjà posté par le wizard legacy à la vente)
  → champ legacy_activation_url sur le Dispatch Job (backfill des 10 tickets TV). Read-only legacy, aucun 2e
  chemin d'écriture → zéro risque de double-facturation (cf. analyse processus).
- store _mapJob : expose legacyActivationUrl ; RightPanel : bouton violet « 📺 Activer STB (Ministra) »
  (MÊME lien que le tech reçoit, ouvre connect_ministra.php avec tid/did/sid/cr/m intacts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:20:09 -04:00
louispaulb
3c17805770 feat(dispatch): coloriage cartes par type legacy + n° ticket dans le détail
- pont : stocke legacy_dept (dept osTicket) sur le Dispatch Job + backfill des 70 existants
- useHelpers.jobColor/legacyDeptColor : palette comme legacy (Installation vert, Réparation or,
  Télé rose, Téléphonie vert pâle, Désinstallation rouge foncé) ; tech en pause = rouge vif prioritaire
- store _mapJob : expose legacyDept + legacyTicketId
- RightPanel : champ « Ticket legacy » (#id · dept) — le client est déjà un lien cliquable vers la fiche
- doc mise à jour

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:05:45 -04:00
louispaulb
30d8b0b29c feat(dispatch): pont legacy(osTicket)→Dispatch Job pour les tickets « Tech Targo »
Tire régulièrement les tickets ouverts assignés au compte « Tech Targo » (staff 3301)
de la DB legacy MariaDB et crée/maj un Dispatch Job ERPNext (pool à répartir).

- lib/legacy-dispatch-sync.js : fetch (status=open AND assign_to=3301) + mapping
  customer (legacy_account_id) / Service Location (coords) / job_type (dept) /
  scheduled_date (epoch→America/Toronto) / start_time (am|pm|HH:MM) / priority
- Idempotent via Custom Field Dispatch Job.legacy_ticket_id (lookup avant create) ;
  ne clobbe pas le travail du répartiteur (maj date seulement si encore open+non assigné)
- SÉQUENTIEL (frappe_pg) ; endpoints GET preview (dry-run) + POST run
- Récurrence opt-in : startSync() au boot, LEGACY_DISPATCH_SYNC=on + _MIN=15
- server.js : route /dispatch/legacy-sync + startSync()
- doc docs/features/legacy-dispatch-bridge.md + index

Mise en service : 70 tickets importés (0 erreur), récurrence 15 min active.

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