Commit Graph

476 Commits

Author SHA1 Message Date
louispaulb
b9df6d720a fix(ops/mobile): bandes d'occupation ≤7 jours remplissent la largeur (fini le micro-défilement)
Les cellules de jour étaient à largeur fixe (52px) → une bande de 7 jours (~400px)
débordait de ~57px sur mobile et défilait légèrement (6,5 jours visibles).

Override mobile `.os-day { flex:1 0 40px; width:auto }` : jusqu'à 7 jours, les
cellules s'étirent pour remplir la largeur (tableau de bord « cette/prochaine
semaine » = 7 tiennent pile). Au-delà (tournées = bande 14 j) elles gardent leur
plancher 40px et la bande défile comme avant.

Vérifié 375px : tableau de bord 7 j = ne défile plus (jour 41px) ; Planif 14 j =
défile toujours (jour 40px). Portée mobile uniquement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:58:43 -04:00
louispaulb
5a35f64a9d fix(ops/mobile): Network GPON tient sur mobile (onglets défilants + arbre enroulé + boutons tactiles)
NetworkPage débordait de ~118px à 375px. Deux causes corrigées :

- Barre d'onglets (4 onglets = 435px) non bornée dans une rangée flex → ne
  déclenchait pas le défilement natif de Quasar. Ajout global `.q-tabs { max-width:100% }`
  en mobile (app.scss) : Quasar réactive son scroll (aucun effet là où déjà borné).
- Arbre GPON : en-têtes OLT/slot/port en flex non-enroulé (nom·IP·adresse·nb
  clients·SNMP·slots·éditer) → débordement. `flex-wrap:wrap` + neutralisation du
  `margin-left:auto` des compteurs sur mobile.
- Boutons d'action de l'arbre (éditer OLT, ajouter port, éditer lieu) étaient
  révélés au survol (opacity:0) → invisibles au tactile. Toujours visibles < 600px.

Vérifié à 375px : docScrollW 493→375, en-têtes enroulées, crayon éditer visible.
Portée strictement mobile (@media max-width:599px) — desktop inchangé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:56:11 -04:00
louispaulb
68063a9f69 refactor(ui): T7 — extraire CustomerSatisfaction de ClientDetailPage (faible couplage)
Premier découpage de la fiche (2102 l) : bloc « Satisfaction » (étoiles/commentaire + « Demander une évaluation »)
→ components/customer/CustomerSatisfaction.vue (props latest/count, émet 'draft'). Vérifié sur la fiche (rend « Aucune
évaluation » à l'identique, 0 erreur). Extraction DÉLIBÉRÉMENT petite + vérifiable ; les gros blocs couplés
(LocationCard/PaymentSection/DeviceStrip — 10+ méthodes parentes : getDevice/combinedStatus/isOnline… + données ACS live
à vérifier) restent à faire en passe dédiée. Build vert, leak 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:22:52 -04:00
louispaulb
936152d452 feat(ui): T5 — DynamicFilter sur Factures fournisseurs + File courriels
- SupplierInvoices : toggle statut → <DynamicFilter> (chips À traiter/Créées/Ignorées + recherche fournisseur/n°/expéditeur, client-side) ; presets 'filter.supplier-invoices'.
- EmailQueue : select statut → chips (Not Sent/Sending/Sent/Error, → rechargement serveur via onFilterChange) + recherche destinataire/référence (client, displayRows) ; presets 'filter.email-queue'.
Build vert, leak 0. DynamicFilter désormais sur 6 pages (Clients/Tickets/Évaluations/RDV/Factures fourn./File courriels).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:02:11 -04:00
louispaulb
f6f4303e2e feat(ui): T5 — DynamicFilter sur RDV (worklist : recherche + chips Type)
Worklist de rendez-vous : recherche (job/client/lieu) + chips service_type (dynamiques) + presets ('filter.rdv') ;
`filteredJobs` applique le filtre APRÈS la portée `view` (à planifier/à recontacter/tous), qui reste un toggle séparé.
Client-side, réactif. Build vert, leak 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:36:32 -04:00
louispaulb
7def94199c feat(ui): T5 — DynamicFilter sur Évaluations (mode client-side)
Recherche client/commentaire + chips Note (5★/3–4★/≤2★) + presets ('filter.evaluations'). Prouve le mode CLIENT-SIDE :
le computed `sorted` filtre (matchNote + recherche) puis trie ; réagit à filterState sans rechargement.
Build vert, leak 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:29:54 -04:00
louispaulb
4d940eefc4 feat(ui): T5 — DynamicFilter sur Tickets (recherche + chips statut/type/priorité + presets)
Remplace search + 3 q-select par <DynamicFilter> ('filter.tickets') ; chipGroups computed (types dynamiques) ;
statut défaut = not_closed préservé ; mappe l'état → refs existantes (search/statusFilter/typeFilter/priorityFilter) → resetAndLoad débouncé.
La PORTÉE « Mes tickets / Mes départements / Tous » reste un toggle séparé (logique de scope/pagination distincte). Compte redondant retiré.
Vérifié préview : 3 groupes, « Non fermés » actif par défaut, chip Urgent 542→3, toggle portée conservé. Build vert, leak 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:26:57 -04:00
louispaulb
3d52ae32c4 feat(ui): T5 audit — composant DynamicFilter réutilisable + intégré sur Clients
- DynamicFilter.vue : recherche + groupes de chips (tag/statut/…) + PRESETS sauvegardés par utilisateur
  (useUserPrefs, clé presetKey) ; contrôleur d'état ({search,chips}) émis → le parent applique (client OU serveur).
- ClientsPage : remplace search+toggle par <DynamicFilter> ; AJOUTE chips Statut/Groupe/Territoire (avant : statut seul) → pilotent la requête serveur ; presets 'filter.clients'.
- fix(erp): countDocs comptait FAUX avec or_filters (get_count ignore or_filters → 0 pendant une recherche) → compte via get_list quand or_filters présent. Vérifié : « bourdon » 0→33.

Vérifié préview : chips (Actifs 9008), recherche (bourdon 33 = lignes), menu presets. Build vert, leak 0. Réutilisable : Tickets/Network/Évaluations/Boîte ensuite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:13:37 -04:00
louispaulb
8f11d41a42 feat(ui): T3 audit — anti-débordement mobile app-wide (bornes app.scss @<600px)
Champs à largeur fixe inline (style="width:NNNpx") bornés à max-width:100% (jamais étirés de force) ;
q-menu .q-list borné ; boutons icône denses ≥32px (cible tactile) ; utilitaires opt-in .ops-row-wrap / .ops-clamp.
(Dialogues + menus déjà couverts par audit #1.) Vérifié à 375px : fiche/Tickets/Rapports/Sous-traitants ≈0 débordement
horizontal, aucune casse visuelle. NOTE : Network reste large (structurel — carte cytoscape/arbre GPON → traité par-page en T7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:50:42 -04:00
louispaulb
7ab71ce6a8 refactor(ui): T1 (suite) — unifier les formatters variants vers les canoniques (formats cohérents)
Sur autorisation user (changement d'affichage OK si ça améliore l'UI) :
- date+heure → formatDateTimeShort : Settings, GiftsInventory, EmailQueue
- date → formatDate : Subcontractors
- initials → staffInitials (1er+dernier) : Historique
- fmtDur → useHelpers.fmtDur («30m»/«1h30») : RendezVous
LAISSÉS (UX volontaire/niche) : ConversationPanel formatDate (relatif auj/hier), PublishScheduleModal (d/m compact),
TechTasks fmtDate (weekday terrain), Planif fmtH (axe timeline), SupplierInvoices fmtMoney (multi-devise).
Build vert, leak 0 ; 12 copies locales éliminées au total (T1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:42:17 -04:00
louispaulb
1017cab649 refactor(ui): T1 audit — dédup formatters → useFormatters (0 changement d'affichage)
3 formateurs canoniques ajoutés (initials [2 premiers mots, '?'], fmtClock [«8:30»→«8h30»], formatDateLong)
+ remplacement des copies locales IDENTIQUES (vérifiées à l'impl) :
- initials : PlanificationPage + RouteMap
- fmtTime → fmtClock : JobCard + TechTasksPage
- formatDate → formatDateLong : ReportRevenueExplorer + ReportInternetCher
Les VRAIES variantes laissées telles quelles (staffInitials 1er+dernier ; fmtDate string ; fmtDur RendezVous ;
fmtMoney multi-devise ; formatDate Settings/ConversationPanel/Gifts date+heure). Build vert, leak 0.
Première tranche de l'audit workflow 26 pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:31:18 -04:00
louispaulb
112ed0ea2b fix(ui): fiche client — en-tête ne déborde plus en mobile ; assistant = sélecteur recherchable
- CustomerHeader : sous-ligne (nom/legacy/type/langue) était `no-wrap` → débordait sa colonne et
  chevauchait le bloc statut/liens en mobile. Fix : wrap + colonne `min-width:0` + le bloc statut/ERPNext/Users
  passe pleine largeur en dessous (<sm) via `col-12 col-sm-auto` ; titre en ellipsis.
- Ajouter un assistant : q-select brut → <TechSelect> (recherche typeahead) ; jdTeamOptions = nom · compétences
  (searchable par nom OU compétence), CAPABLES d'abord (possèdent la compétence du job), value = id ; jdAddAssistant résout le tech par id.

Vérifié préview mobile (375px) : sous-ligne wrap, 0 débordement (scrollW==clientW) ; build vert, leak 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:31:44 -04:00
louispaulb
80c8005f0c feat(planif): symbole de compétence du job sur les chips « à répartir » (Tournées)
Remplace l'icône terrain/distant par le SYMBOLE de compétence (skillSym : live_tv/build/cable/cell_tower/camion-nacelle…, couleur = getTagColor) — cohérent avec les cartes kanban + blocs de tournée. Vérifié préview : chips = live_tv (TV), build (réparation)…

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:16:12 -04:00
louispaulb
c8afa97004 feat(planif): Tournées — chips secteur + jobs non assignés sur la carte (gouttes), filtre liste+carte
Batch D — regrouper/situer les non-assignés du jour :
- RouteMap : nouvelle prop `pins` (+ renderPins/clearPins/watch, gouttes oranges couleur=priorité, clic → @pin-click) — miroir de `live`, inclus dans le fit bounds
- Planif Tournées : chips SECTEUR (par ville, comptés, triés) qui filtrent SIMULTANÉMENT la liste « à répartir » ET les gouttes sur la carte ; routeUnassignedPins (coords robustes latitude/lat), clic goutte → détail ; « Suggérer » agit sur le secteur si filtré

Vérifié préview : jour 06/07 → 10 secteurs + 21 gouttes ; clic « STE CLOTILDE » → 4 chips + 4 gouttes (« 4 à répartir / 21 »).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 10:06:34 -04:00
louispaulb
21724e65f8 feat(planif): Tournées — filtre compétence sur la carte + jour ciblé + jobs non assignés en évidence
Batch B — le filtre de compétence pilote la carte : dayLivePositions (GPS techs) filtré par visibleTechs
(les tournées l'étaient déjà via visibleTechs) → seuls les techs capables + leurs jobs sur la carte.

Batch C — mise en évidence des non-assignés du jour :
- Badge rouge « dus non assignés » par jour sur la bande (OccupancyStrip.os-unassigned), depuis unassignedByDay
- routeDayUnassigned = jobs dus le jour sélectionné mais non assignés → bande « N à répartir » (chips cliquables → détail) + bouton « Suggérer » ; se met à jour au clic sur la strip

Vérifié préview : positions filtrées par compétence ; badges [1,21,16,15,3,8] sur la bande ; clic 06/07 → bande « 21 à répartir » (14 chips réels).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:58:58 -04:00
louispaulb
012f924683 feat(planif): simplification barre du haut — Suggérer (jour sélectionné), fix recherche X, retrait Équipe
- Bouton « Générer » (haut) → « Suggérer » (openSuggest) basé sur le jour de la vue ACTIVE (strip Tournées / Jour kanban / bande mobile). Le solveur de quarts est déplacé dans Outils (« Générer l'horaire (semaine) », doGenerate conservé).
- Fix recherche tech : le X (clearable) mettait search=null → `search.value.trim()` plantait le computed visibleTechs → le champ « ne se vidait pas ». Rendu null-safe.
- Retrait du sélecteur « Équipe » (groupOptions vide : aucun tech n'a de `group` ; le filtre par compétence suffit).

Vérifié préview (desktop 1400px) : X vide le champ sans planter, Suggérer ouvre le dispatch auto, « Équipe » absent, grille rendue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:47:42 -04:00
louispaulb
d385367095 feat(hub): lib/vacation-import — lecteur .xlsx (ZIP/zlib, sans dép) + parseur vacances + matcher techs
Cœur de l'import du calendrier de vacances (cf. memory project_vacation_import) :
- unzip() : lecteur ZIP minimal via zlib.inflateRawSync (aucune dépendance npm, comme gmail.js évite googleapis)
- parse xlsx : sharedStrings + styles(fills→couleur) + cellules ; couleur cyan/jaune/magenta = Choix1/Choix2/Approuvé (vert=instruction, ignoré)
- parseCell() : texte de cellule → jours ISO (« 6 et 7 », « 6 au 10 », « 13,14…24 », « 21 juin au 1er juillet », listes .-) ; résolution du quantième par proximité (gère bornes de mois + listes multi-semaines)
- extractPeople() : personnes → jours par statut, plages contiguës
- matchAll() : appariement noms→techs en 2 passes (certains d'abord, puis lever ambiguïtés sur techs restants ; indice = initiale de nom)
- buildPreview() : buffer + techs → payload dry-run

Vérifié EN LOCAL (node, runtime hub) contre le vrai fichier « Vacances 2026.xlsx » : 30 semaines, 24 personnes, 24/24 appariées ; ambiguïtés Louis/Simon + cellules texte-sans-couleur correctement signalées « À classer ». Endpoints + UI OPS = étape suivante.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 09:26:57 -04:00
louispaulb
b7c24f58b2 feat(planif): sélecteur de jour « Tournées » = 2 semaines par défaut
routeStripDays passe de 7 → 14 jours (depuis le lundi affiché), données via /roster/capacity
(14 j, découplé de la fenêtre de charge 7 j du grid → barres réelles en semaine 2). Chargé à
l'entrée de la vue Tournées + au changement de semaine (watch [boardView, start]). Bande pleine
largeur + défilement horizontal ; jour courant marqué.

Vérifié préview : 14 cellules (29/06→12/07), today marqué, scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:55:29 -04:00
louispaulb
5491aa8abe feat(dashboard/planif): vue 2 semaines + deep-link jour + slot-finder « Planifier des quarts »
- Tableau de bord : 2 bandes d'occupation (« Cette semaine » / « Semaine prochaine », 14 j depuis lundi), jour courant marqué (liseré indigo dans OccupancyStrip). Clic jour → /planification?day=ISO
- Planif : deep-link `?day=` (cadre la semaine + sélectionne le jour grille/mobile/tournées) & `?skill=` (filtre compétence, match casse-insensible vs allSkills) via focusDay() lu au montage
- Slot-finder (SuggestSlotsDialog) : état « Aucun créneau » → bouton « Planifier des quarts pour « X » » → émet plan-shifts → Planif ouvre la timeline (vue grille) au jour cherché, filtrée sur la compétence, + toast « crée un quart pour ouvrir des créneaux »

Vérifié préview bout-en-bout : 2×7 jours + today, clic → ?day=2026-07-09 cadre 06→12/07, recherche vide → bouton → dialog fermé + grille + toast compétence. Build vert + leak-check 0. Aucun changement hub.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:34:43 -04:00
louispaulb
9a507d35db feat(dashboard): bande d'occupation + KPI cliquables/tendance + garde-fou PPA ; barres de charge en-tête Planif
- <OccupancyStrip> réutilisable (extrait de .pm-strip) ; heatColor déplacé dans useHelpers = source unique
- Tableau de bord : bande « Charge à venir » (7 j glissants, réutilise /roster/capacity) ; clic jour → Planif ; légende + heures à répartir
- KPI cliquables (→ page liée) + sous-ligne tendance/contexte (urgents, +N tickets 7 j, non assignés)
- Garde-fou PPA : confirmation explicite avant prélèvement (cf. incident double-charge 2026-06)
- Planif : barres verticales d'occupation dans l'en-tête desktop (hdr-vbar) + sélecteur de jour « Tournées » = OccupancyStrip (remplace les chips « lun 29/06 »)

Vérifié en préview (strip, clics jour, dialog PPA annulé sans charge, nav KPI) + build vert + leak-check 0. Aucun changement hub (réutilise /roster/capacity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 08:00:46 -04:00
louispaulb
cf9feb67f4 refactor(ops): retirer la page Dispatch dépréciée — tout sur Planification (−2179 lignes, −240 KB)
Batch 4 de l'audit. La page Dispatch n'était plus utilisée ; Planification la remplace.
- Supprimé DispatchPage.vue (2179 lignes → chunk 240 KB disparu du build).
- router : route /dispatch → REDIRECT vers /planification (anciens signets OK).
- nav.js : entrée « Dispatch » retirée. PlanificationPage : 2 liens « Tableau Dispatch » retirés.
- Dashboard (clic job du jour) + TaskNode (goToDispatch) → redirigent vers /planification.
- src/api/dispatch + src/config/dispatch INCHANGÉS (modules partagés app-wide).
Vérifié : build OK (chunk DispatchPage disparu), nav sans Dispatch (26 items), Planif rend,
/dispatch redirige.

NON appliqué (vérif avant édition = l'audit ne tenait pas) : consolidation initials()
— PAS équivalent (staffInitials = 1re+DERNIÈRE initiale + fallback '' ; local = 1re+2e mot +
fallback '?') → changerait les initiales affichées. Fixes de fuites : composants ont déjà
onUnmounted → « fuites » = cas v-show non confirmés. Laissés tels quels (churn/risque > gain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:03:57 -04:00
louispaulb
e5bd63b2d6 chore(ops): remove unused ResponsiveDialog.vue (0 imports)
Batch 4 (dead code) : composant orphelin (aucun import dans tout src/). Non bundlé
(tree-shaké) → suppression = nettoyage repo pur, aucun impact runtime/bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:43:44 -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
4a641ac802 perf(ops): lazy-load heavy libs & shell components — chunks route/shell bien plus légers
Batch 1 de l'audit d'optimisation. Les libs lourdes étaient statiquement importées
donc livrées même inutilisées → passées en import dynamique / defineAsyncComponent :
- MainLayout : ConversationPanel/PhoneModal/FlowEditor/NewTicket/Orchestrator/
  ServiceStatus/OutboxPanel → async (chunk shell 312 KB → 56 KB, chargé sur CHAQUE page)
- TaskGraphPage : TaskGantt (hy-vue-gantt) → async (2.6 MB → 20 KB ; le Gantt ne charge
  que dans la vue Gantt)
- NetworkPage : cytoscape → import() dans loadNetworkMap (484 KB → 52 KB)
- ReportAR/Revenu/Taxes : chart.js/auto → import() dans renderChart (async) (~150→~10 KB/page)

Total dist inchangé (~8 M) — le code est DÉFÉRÉ en chunks à la demande, pas supprimé.
Gain réel = ce que l'utilisateur télécharge pour VOIR une page. Build OK, leak-check propre,
shell + route rapport vérifiés (aucune erreur d'import).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:35:30 -04:00
louispaulb
1af8c62142 feat(planif): géofencing des jobs — timeline En route/Arrivé/Reparti (suivi façon colis)
Le hub compare la position GPS live de chaque tech (Traccar) aux jobs du jour →
détecte les transitions et les journalise par job. Le statut « live » du job est
DÉRIVÉ du journal (compute-on-read, aucune écriture ERPNext — pas de valeur « en
cours » côté doctype). Fiche job : timeline horizontale Assigné → En route → Arrivé
(HH:MM) → Reparti (HH:MM).

- lib/geofence.js : runScan() (rayon 150 m, sortie 230 m hystérésis, séjour 3 min),
  store data/geofence.json par job, machine à états ; startScan() scheduler 90 s
  (GEOFENCE_SCAN=off pour couper). Vérifié prod : 12 jobs × 14 positions, transitions OK.
- server.js : démarre le scan au boot.
- roster.js : GET /roster/job/:name/geofence (timeline) + /geofence-states + /geofence-scan (test).
- api/roster.js : jobGeofence(), geofenceStates().
- PlanificationPage : openJobDetail charge la timeline ; stepper .jd-geo (scoped).

Suite possible : écrire le vrai statut ERPNext à l'arrivée (nécessite d'ajouter une
valeur « En cours » au doctype Dispatch Job) — non fait pour éviter le risque schéma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:34:55 -04:00
louispaulb
d966e1b749 feat(planif): positions GPS live des techs (Traccar) sur la carte des tournées
Onglet Tournées : marqueurs « live » de chaque tech (appareil Traccar associé),
rafraîchis ~25 s tant que l'onglet est ouvert. Marqueur = pastille ronde à initiales,
couleur = celle de la tournée du tech, halo pulsé si position fraîche (grisé sans halo
si > 10 min). Tooltip : nom · « il y a X min » · vitesse (km/h) ou « arrêté ».
Bouton « GPS live (N) » pour afficher/masquer. Respecte les chips techs masqués.

- hub roster.js : GET /roster/tech-positions → {positions:[{techId,techName,lat,lon,time,speed}]}
  (batch getPositions par appareil). Vérifié prod : 14 positions.
- api/roster.js : techPositions().
- RouteMap.vue : prop `live` + marqueurs .rm-live (halo pulsé, staleness), watch dédié.
- PlanificationPage : polling 25 s (démarré sur l'onglet Tournées, arrêté sinon +
  onUnmounted), couleur mappée depuis dayRoutes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:06:16 -04:00
louispaulb
c74462f97b feat(planif): dispatch auto = SEULEMENT les jobs du jour sélectionné (retards exclus par défaut)
Sélectionner le 7 juillet ne devait pas tirer les jobs en retard/sans date (souvent
faits mais pas fermés) dans la simulation. Par défaut (aucun chip de date coché),
suggestJobs ne garde que les jobs DATÉS sur le(s) jour(s) de la fenêtre — plus de
pollution par les retards/autres jours. Pour dispatcher les retards : cocher le chip
«  en retard » (comportement inchangé). Le job_names envoyé au solveur hub ne contient
alors que les jobs du jour → même résultat côté Optimiser.

Vérifié preview : jour 07-07 → 15 jobs, plan = 15 arrêts tous le 07-07, 0 en retard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:56:56 -04:00
louispaulb
3a836cfb21 fix(ops): HUB_URL = /ops/hub en prod (fin du menu vide — la VRAIE cause)
RACINE du « Accès OPS indisponible / menu vide » : le bundle appelait `/hub/...`
au lieu de `/ops/hub/...`. Cause : `.env.local` contient `VITE_HUB_URL=/hub`
(pour le proxy du serveur de dev) et Vite charge .env.local MÊME au build prod
(même fuite que VITE_DEV_USER) → HUB_URL="/hub" inliné. `/hub` (hors préfixe /ops)
est routé vers ERPNext → **HTTP 404** sur /hub/auth/permissions → can()=false →
menu/recherche/avis vides. Preuve navigateur : app `/hub/...`→404, direct
`/ops/hub/...`→200 superuser.

Fix : config/hub.js n'honore VITE_HUB_URL qu'en DEV ; en PROD HUB_URL =
BASE_URL + '/hub' = **/ops/hub** (chemin conçu, proxifié par nginx → hub, vérifié 200).
Bundle vérifié : plus de "/hub" standalone, tout est BASE_URL+"/hub".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:43:44 -04:00
louispaulb
7b54e87a59 fix(ops): overlay accès — message exact « n'a pas pu charger » + détail erreur + piste extension
L'overlay disait à tort « pas de permissions » alors que le compte y a droit et que
le fetch /auth/permissions échoue (blocage client). Message corrigé : « OPS n'a pas pu
charger les permissions » + affiche le détail technique (usePermissions.error) + suggère
fenêtre privée/autre navigateur (une extension bloque souvent /auth/permissions). Rend
chaque capture d'écran auto-diagnostique.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:31:41 -04:00
louispaulb
01bcd6476d fix(ops): la déconnexion ramène sur OPS après login (plus la page Authentik)
logout() ajoute `?next=<url OPS>` au flow d'invalidation Authentik : après
déconnexion, Authentik renvoie vers erp.gigafibre.ca/ops → qui (non authentifié)
redéclenche le login AVEC chemin de retour → après login on revient sur OPS,
au lieu d'atterrir sur /if/user/#/library. Sans régression si Authentik ignore
`next` (= comportement actuel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:25:41 -04:00
louispaulb
f5d8d38b1e fix(ops): retry permission load (superuser plus jamais bloqué par un échec transitoire au boot)
Symptôme (overlay fail-loud confirmé) : « OPS a reçu louis@targo.ca mais pas de
permissions », alors que le compte EST superuser (is_superuser=True, admin+sysadmin,
vérifié live) et que le hub renvoie 200 (6/6, ~0,14 s). La recherche (même base /hub,
appelée plus tard) marche → seul le 1er appel /hub au démarrage (checkSession) échoue
= course de boot / session pas encore « chaude ».

Fix : loadPermissions réessaie jusqu'à 4× (backoff 0,7→2,1 s, cache:no-store) avant de
conclure « pas d'accès ». Un échec transitoire ne verrouille plus l'app derrière un
menu vide / overlay. (Les droits sont dans Authentik : groupes admin/sysadmin + is_superuser,
inchangés — ce n'était ni le jeton ni les droits.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:18:53 -04:00
louispaulb
e6ac8b11ef fix(ops): fail-loud when OPS perms don't load + username fallback (supersede 96da97b session overlay)
Root cause of the recurring "blank menu": the hub keys OPS permissions to the
exact Authentik email (louis@targo.ca). If whoami passes a different email
(duplicate account e.g. louism@targointernet.com) or an empty email, the hub
404s → can()=false → menu/search/avis silently blank, mistaken for a token bug.
The ERP token and Authentik provider config were verified correct throughout.

- REVERT the misleading session-fix (96da97b): drop verifySessionAlive /
  sessionExpired / auto-reload churn and the "Session expirée" overlay; restore
  the original keep-alive ping. (Re-login couldn't fix a perms/identity issue,
  so that overlay only added noise.)
- FAIL-LOUD (App.vue): when permissions don't load in prod, show a clear overlay
  naming the exact account OPS received ("OPS a reçu <email>, pas de permissions")
  + Changer de compte / Recharger — instead of a silent blank shell.
- USERNAME FALLBACK: whoami username is captured (api/auth getAuthUsername),
  passed to loadPermissions, and the hub (/auth/permissions) retries by username
  when the email matches no provisioned OPS account. Verified on prod: username=Louis
  alone resolves; bad-email+username falls back; unknown → 404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:49:26 -04:00
louispaulb
96da97bfad fix(ops): session expirée → overlay clair + ré-auth, plus de menu blanc silencieux
Quand la session Authentik expire, nginx /auth/whoami renvoie 200 avec un
courriel VIDE (pas un 401) → ni _reauth ni le keep-alive ne le voyaient →
getLoggedUser retombait sur le propriétaire du jeton ERP, loadPermissions
était sauté, can() = false partout → menu/recherche/avis vides EN SILENCE
(l'app avait l'air cassée alors que le backend était sain).

- api/auth.js : verifySessionAlive() lit whoami (rafraîchit aussi la fenêtre
  Authentik) ; 200 + courriel vide confirmé par une 2e lecture (filtre les
  blips) = session morte → 1 rechargement auto (Traefik → login Authentik),
  sinon état sessionExpired (anti-boucle 30 s). getLoggedUser renvoie '' pour
  une session morte au lieu de masquer avec « Administrator ».
- stores/auth.js : checkSession relance verifySessionAlive si courriel vide.
- useSessionKeepAlive : le battement passe par verifySessionAlive (détecte
  enfin la mort par courriel-vide, pas seulement les 401).
- App.vue : overlay « Session expirée → Se reconnecter » au lieu d'une coquille
  vide. Inerte en dev local (localhost court-circuité).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:28:09 -04:00
louispaulb
8d80b44a79 feat(planif): « Suggérer » respecte aussi le filtre de TYPE du pool
Étend le filtre du dispatch auto au type affiché (« ce que je vois = ce que
je répartis ») : suggestJobs part désormais de assignJobsFiltered (type + date)
au lieu des seules dates. Défaut inchangé (aucun filtre = tout le pool part).
Levier concret : décocher « téléphonie » → non réparti aux techs terrain.

- suggestJobs : base = assignJobsFiltered (type ∩ date) hors On Hold ; lasso prioritaire.
- suggestFiltered + suggestScope : bouton « Suggérer (N) » et dialogue
  « N job(s) des jours + types cochés → jours » quels que soient les filtres actifs.
- Aucun changement hub : optimize-plan filtre déjà par job_names (déployé).

Vérifié preview : réparation seul → 18 ; réparation + 07-06 → 5 arrêts,
tous réparation, tous le 07-06 (heuristique locale).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:09:18 -04:00
louispaulb
6c5ed0060f feat(planif): « Suggérer » ne répartit que les jobs des jours cochés
Le dispatch auto utilisait tout le pool avec son propre sélecteur de jour,
en ignorant les chips de date du pool (et même la sélection lasso, sur le
chemin solveur hub). Désormais les chips pilotent le dispatch :

- FENÊTRE = les chips de date cochés ( en retard / Sans date → placés
  aujourd'hui) ; aucun chip → repli sur le sélecteur Aujourd'hui/Demain/date.
- JEU DE JOBS restreint aux jours cochés (ou à la sélection lasso) sur les
  3 chemins : heuristique locale, solveur local, ET solveur hub (nouveau
  param `job_names` dans optimize-plan, additif/rétrocompatible).
- UI : bouton « Suggérer (N) » + dialogue « N job(s) des jours cochés → jours ».
- Helper partagé jobMatchesDateChips (dé-dup avec le filtre d'affichage).

Vérifié en preview : chip 07-06 → 19 arrêts tous le 07-06 (local) ;
chips 07-06+07-07 → 34 arrêts, uniquement ces 2 jours (solveur hub déployé).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:58:38 -04:00
louispaulb
c209098a44 fix(security): stop baking dev-user email into the prod bundle
VITE_DEV_USER (.env.local = louis@targo.ca) was inlined by Vite into
auth.js's localhost fallback, shipping personal PII in every prod build.
Gate that branch behind import.meta.env.DEV so esbuild dead-code-eliminates
it (and the inlined string) from production builds — permanent, no reliance
on a build flag. Also neutralize the two hardcoded louis@targo.ca in the
campaign TemplateEditor (merge-tag sample → exemple@gigafibre.ca; test-send
default → empty, send button already guards on non-empty). Leak-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:36:04 -04:00
louispaulb
69f00840c8 feat(planif): hybrid recurring shift model — weekly_schedule pattern + materialization
The weekly_schedule pattern per tech is now the SOURCE of shifts. New
materializeShifts() (hub roster.js) generates source='pattern' Shift
Assignment rows over an N-week horizon, auto-skipping QC holidays
(holidays-qc) and vacations (Tech Availability), and PRESERVING manual
overrides (source≠'pattern') — e.g. a one-off night week. Idempotent
(re-runnable / cron-ready).

- hub: fetchTechnicians returns parsed weekly_schedule; ensureShiftTemplate
  (find/create Shift Template by exact times); endpoints
  POST /roster/technician/:id/weekly-schedule and /roster/materialize-shifts
  (GET=dry-run, POST=apply, weeks/tech params).
- api/roster.js: setWeeklySchedule + materializeShifts.
- WeeklyScheduleEditor: mode toggle 🔁 Récurrent (base, materialized) ↔
  📌 Exception (concrete override weeks); adaptive labels/horizon.
- PlanificationPage onScheduleApply branches: recurring → setWeeklySchedule
  per tech + materializeShifts; exception → direct-write source='manuel'.

No ERPNext schema change (weekly_schedule + source already exist).
Dry-run against prod: 6 techs patterned → 48 shifts/2wk, 0 collisions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:35:57 -04:00
louispaulb
c267f9a3dd feat(planif): functional dispatch — single-day suggest, one overdue chip, sick-tech redistribute, bulk shift gen
Making the auto-dispatch match the real workflow:
- SINGLE selected day: suggestWindow is now one day (suggestDay), not
  today+tomorrow. openSuggest defaults it to the board's focus day
  (mobileSelIso/kbSelIso/today); config has a day picker (Aujourd'hui /
  Demain / date). Overdue + undated jobs are pulled onto that day.
  Re-simulating today is fine (non-destructive; apply is explicit).
- ONE " en retard" chip: assignDates collapses all past dates into a
  single overdue chip (was one per date); pool filter handles __overdue__.
- SICK/absent tech: a "sick" button per review group deselects the tech
  and re-optimizes → the solver redistributes their jobs across the rest
  (verified: removing Anthony reassigned his jobs, Gilles picked up).
- BULK shift generation (A): WeeklyScheduleEditor gains multi-tech mode
  (tech chips, all on by default); "Générer les horaires (lot)" in the
  Outils + mobile menus applies a template to N techs × N weeks at once.

Verified: overdue collapses to "en retard 13"; day selector + single-day
plan (13 jobs); sick redistributes; bulk shows 56 techs / "Générer ×56".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:59:39 -04:00
louispaulb
b1ff46e8c1 feat(planif): weekly shift generator per tech (templates + N weeks) — reworked from Dispatch
The best schedule editor lived in the (to-be-deleted) Dispatch page.
Reworked it into a shared Quasar WeeklyScheduleEditor.vue:
- Reuses the shared useHelpers.SCHEDULE_PRESETS (5×8h / 4×10h / 3×12h)
  as one-click chips that fill a per-day form (toggle + start/end + hrs).
- "Générer pour N semaine(s)" → automation: repeats the pattern over N
  weeks from the displayed week.
- Emits the schedule; Planif writes Shift Assignments DIRECTLY (published)
  via ensureWindowTpl (per distinct window) + roster.createShift
  (idempotent), then refreshes the grid. No Publier round-trip.

Wired into the tech skill dialog ("Générer l'horaire…", + a "5×8h
rapide" quick path). This is what makes the créneau/skill-shift feature
usable (regular shifts now exist). Dispatch's schedule modal can be
retired — its useful bits (SCHEDULE_PRESETS) were already shared.

Verified: editor opens per tech with presets + 7 day rows + weeks input;
matches the target UX in Planification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:37:46 -04:00
louispaulb
8b9d5376a7 feat(booking): slot search respects skill + real shifts + weekend-for-repairs
suggestSlots was skill-blind and assumed a default 8-17 shift for every
tech. Now:
- SKILL FILTER: only techs whose skills (or _user_tags) include the
  requested skill are considered. Dialog passes the picked chip's skill.
- REAL SHIFTS: slot windows come from published Shift Assignment ×
  Shift Template (garde/on_call excluded). A tech with no regular shift
  that day yields no slot — so it reflects reality (needs shifts created).
- WEEKENDS reserved for repairs: Sat/Sun skipped unless the skill is a
  repair/dépannage. Removed SLOT_DEFAULT_SHIFT.
- Empty-state message hints to publish a shift / weekend rule.

Verified: with only garde shifts in the horizon, correctly returns 0
(regular_count=0). Skill/weekend/garde-exclusion logic confirmed via
data diagnostic. Produces slots once a regular day-shift is published for
a qualified tech.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:21:01 -04:00
louispaulb
374e0da229 feat(ui): simpler créneau dialog — skill chips + defaulted duration + progressive disclosure; fix address autosuggest everywhere
Applying the "one-click defaults + reveal-on-input + less clutter" pattern
the user asked for, on the SuggestSlotsDialog:
- Skill chips (4 main, from assignTypes + getTagColor, passed by parent):
  one click picks the skill AND sets the default duration (SLOT_SKILL_DUR:
  install 2h, réparation/tv 1h, téléphonie 0.5h…), adjustable.
- Progressive disclosure: duration/date/search appear only after an
  address is chosen (hint otherwise). Less useless info up front.
- Selected skill flows into the created job as a tag.

Fix (autosuggest broken everywhere): the dialog used a q-menu (needs a
click trigger, so v-if never opened it) → switched to the q-list-below
pattern. More importantly, useAddressSearch pointed at ERPNext
/api/method/search_address (403/perm) → repointed at the reliable hub
/rpc/search_addresses (Postgres rqa_addresses, works in dev+prod via the
/hub proxy). Fixes autosuggest in UnifiedCreateModal + DispatchPage too.

Verified end-to-end: type "54 chateauguay huntingdon" → suggestions
appear → pick → Installation chip sets 2h → search returns 8 slots
(08:15–10:15). Chips + reveal + green ✓ on validated address.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 09:58:35 -04:00
louispaulb
df23f232b3 feat(planif): native OPS job creation (#17) — Dispatch deprecated, create from Planification
Job/installation creation now lives in Planification (Dispatch board is
being retired). Reuses the shared UnifiedCreateModal (work-order mode:
subject, job_type, address+RQA geocode, duration, skills/tags, tech,
date) so it's the same proven form.

- "Créer un job" button (desktop toolbar + mobile ⋮ menu) → modal →
  createJob → @created refreshes the pool ("À assigner") + occupancy.
- "Créneau" button → SuggestSlotsDialog (P1-C, re-homed from Dispatch) →
  pick tech+date+time → prefills the create modal.
- useUnifiedCreate: work-order flow now carries start_time + prefilled
  coords (from a booked slot) through resetForm → payload.

Sync F transitoire: OPS-native jobs have NO legacy_ticket_id, so the F→
OPS sync and purgeStaleOrphans (which only touch legacy_ticket_id jobs)
never clobber them — they coexist. No F push for now (billing stays via
the sales path).

Verified: modal opens in Planification with all 9 work-order fields;
button in toolbar (gt-sm) + mobile menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 09:05:32 -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
28dff24227 feat(dispatch): P1-A SLA on Planification jobs — reuse existing SLA system
SLA was already built (missed by Gemini's plan): hub /sla/policies store,
useSla() composable (slaFor → ok/at_risk/breached + SLA_BADGE), editable
SlaSettings.vue — used on tickets. Surfaced it on Planification jobs by
reuse (NO ERPNext schema migration, NO new cron):

- jobSlaBadge(job): maps a Dispatch Job to a synthetic ticket
  (priority→capitalized, creation, first_responded_on set so it uses the
  RESOLUTION band = creation + policy[priority]) and returns only the
  actionable states. entrySlaBadge for review entries via poolByName.
- Red "SLA dépassé" / orange "SLA proche" chip on pool jobs (triage) and
  review entries; ok/none show nothing (no clutter).

Compute-on-read, so the matrix stays editable in Settings with zero
migration risk. Verified: 57 breached + 5 at-risk on the current pool
backlog; chip renders next to the job subject.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 07:39:33 -04:00
louispaulb
3599ec995f feat(dispatch): P1-C "Trouver un créneau" — wire suggest-slots to a booking dialog
The hub /dispatch/suggest-slots (gap-finding + travel buffer + proximity
ranking) existed but had NO UI (Gemini claimed SuggestSlotsDialog.vue was
coded — it did not exist). Built it:

- api/dispatch.js: suggestSlots(payload) → POST /dispatch/suggest-slots.
- SuggestSlotsDialog.vue: address (RQA geocode via useAddressSearch) +
  duration + earliest date → ranked slots (tech, date, time, travel min,
  reasons). Selecting a slot emits it.
- DispatchPage: "📅 Créneau" button in the header opens the dialog;
  onSlotSelected prefills the WO create modal (tech + date + address +
  duration) and confirmWo injects the slot's start_time + geocoded coords
  into createJob. Manual "+ WO" clears the booking slot.

Verified: dialog compiles/opens with inputs; suggest-slots returns ranked
slots (e.g. Gilles Drolet 08:15–09:45, journée libre, 15 min). Note: the
address geocode (/api/method/search_address) 403s in the dev preview
because the dev proxy doesn't inject the ERP token that prod nginx adds —
works in prod (same call the create modal already uses).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 07:30:08 -04:00
louispaulb
7b7980a12b feat(routemap): pill markers — job-type icon + sequence number (best practice)
Route-stop markers changed from a plain number disc to a rounded-
rectangle "pill" carrying BOTH the job-type icon (material-icons) and
the sequence number, in the tech's route color with a white border —
the standard for markers that show two facts (logistics/route tools).
Number stays bold (primary scan key), icon is context.

- markerIcon(skill): map-safe resolver returning ONLY material-icons
  ligatures (skillSym's custom SVGs don't render in a plain <span>);
  install→construction, repair→build, tv→live_tv, phone→call, etc.
- stops in dayRoutes + suggestRoutes now carry icon; RouteMap renders
  <span.material-icons> + number in .rm-pill. Hover fan-out, hover-to-
  front, and cluster logic unchanged (operate on the pill element).

Verified: 43 pills render with glyph+number in tech colors; 8-pin
cluster still fans out on hover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:02:27 -04:00
louispaulb
2177212a67 fix(dispatch): GPS device row was misleading — clear linked-device chip + easy relink
The tech WAS associated (Stéphane → traccar_device_id "33" = device
"24 - Stephane M"), but the q-select's use-input placeholder ("non
associé — choisir l'appareil Traccar") kept showing in the filter input
even with a value selected, so it read as unassociated.

Redesigned the inline GPS row:
- Linked: a clear green chip with a 📍 pin + online dot showing the real
  device name (resolves traccar_device_id → Traccar name), a "Changer"
  menu (filter + list) to relink to ANOTHER vehicle, and a dissociate
  button. jdDevice computed; missing id → "#id — introuvable" (not a
  silent blank).
- Not linked: "non associé" + an auto-suggested device (jdDeviceSuggest:
  Traccar device whose name/uniqueId contains a word of the tech's name)
  one-click "Associer …", plus a "Choisir" picker menu.

Removed the confusing q-select + its filterDevices. Verified: Stéphane's
row now shows "📍 24 - Stephane M" (no "non associé"); Changer menu lists
all vehicles with a filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:56:27 -04:00
louispaulb
fc1f61eecf fix(dispatch): OSRM for ALL routing + fix waypoint overshoot + detail dialog polish
Routing: the last Mapbox routing calls now go through our OSRM —
useMap.computeDayRoute (Dispatch board route lines) → /roster/osrm-route,
useAutoDispatch optimizeRoute → new /roster/osrm-trip (OSRM /trip TSP,
same response shape as Mapbox optimized-trips). 0 Mapbox directions/
matrix/optimized-trips fetches left in the SPA. (Remaining Mapbox =
gl-js renderer + base tiles + geocoding, which OSRM can't provide.)

Anthony route bug: /roster/osrm-route now sends continue_straight=false.
Without it OSRM forbids U-turns at intermediate waypoints, so on a
home→stop→home loop it overshoots the address and turns around in a side
street ("s'éloigne et tourne"). Now it goes straight to the door.
Benefits every map (shared osrm-route).

Job detail dialog:
- Highlighted assigned-tech chip (indigo, avatar+name) in the header —
  "who does this job" is now obvious; grey "Non assigné" otherwise.
- Mini-map shows nearby jobs (pool + occupancy within ~3km) as pale
  grey dots for context, under the job's own pin; centered on the job.
- Normalize displayed strings: decode HTML entities (subject
  "d&#039;équipement" → "d'équipement") via deEnt().

Verified: osrm-trip returns Ok order; detail shows Anthony Dion chip +
decoded subject; routes render (43 pins), Anthony 31.7km/35min round trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:41:00 -04:00
louispaulb
e92c220396 feat(routemap): full ticket detail on pin click + routes return to origin
- Click a pin → the FULL job-detail dialog (address, description, ticket
  thread, team) instead of a mini popup. RouteMap emits 'stop-click'
  (stop payload + route id); stops now carry their job identity
  (suggestRoutes → entry/name, dayRoutes → job/name). Parent wires
  onReviewStopClick (→ openEntryDetail) and onRoutesStopClick (→
  openJobDetail). Verified on both maps (#253081 Tournées, #253286
  review).
- Routes now RETURN to the starting point: line + OSRM geometry + km/min
  close the loop home→stops→home in RouteMap; the review leg list gets a
  "🔄 retour 🏢 bureau TARGO : X min" row (fetchTechRoute appends origin;
  loadLegTimes captures returnTo). The solver already optimized round
  trips; now the displayed distance/time reflect it. Verified: 11 return
  legs shown.
- Fix (0,0) coord blowing out map bounds: suggestRoutes now filters
  |lat|>0.01 (dayRoutes already did) and RouteMap.boundsOf guards okLL —
  review map was zooming to the Gulf of Guinea. Verified: bounds back on
  Montérégie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 20:19:57 -04:00