Le champ « Cadence » global était un FACTEUR brut (step 0.05, bas=rapide) → taper 1000 = facteur 1000
= catastrophiquement lent. Passé en % via effPctOf/factorFromPct (100/x) : 1000 % = facteur 0.1 = 10× plus
rapide, 90 % = plus lent. Renommé « Efficacité % » + tooltip. Le per-skill était déjà en % (inchangé).
NB : l'efficacité n'influe que sur les stratégies heuristiques (Intelligent/Meilleurs/Équilibré/Juste-ce-qu'il-faut),
PAS sur « Optimiser » (VRP OR-Tools) qui optimise routes+compétences+capacité sans facteur d'efficacité.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- nnOrder respecte la fenêtre horaire : bloc AM (8-12) → sans-fenêtre → PM (12-16), plus-proche-voisin
DANS chaque bloc. Avant : NN pur → une job PM pouvait finir 1re, une « premier en AM » 2e (bug signalé).
Utilise le toggle AM/PM explicite OU detectAmpm (texte : « **premier en AM** », « PM Fin de journée »).
- Picker « déplacer vers un autre tech » : n'affiche que les techs SÉLECTIONNÉS + compétents par défaut,
+ « Afficher tous les techniciens… » pour lever le filtre (repli sur tous si aucun sélectionné-compétent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cause : :routes="suggestRoutes.map(...)" inline créait un NOUVEAU tableau à chaque rendu → watch deep
de RouteMap → draw → emit('metrics') → onRouteMetrics mutait realRoutes → re-rendu → nouveau tableau →
BOUCLE INFINIE (onglet figé ; le remplacement asynchrone OSRM ne finissait jamais → lignes à vol d'oiseau).
Fix : computed CACHÉ suggestMapRoutes (réf stable) + onRouteMetrics idempotent. La géométrie routière OSRM
(vérifiée côté serveur : 200, ~405 pts) s'affiche maintenant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le @navigate du panneau ticket routait tout vers loadModalTicket ; désormais dt==='Customer' → fiche.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- roster API : AbortController (timeout 45s, 90s pour /optimize-plan) → un solveur/hub lent ou
injoignable lève au lieu de laisser l'await pendre à l'infini (suggestDlg.building coincé à true).
Au-delà → repli heuristique local, dialogue utilisable.
- q-dialog Suggérer : @hide remet building=false (jamais coincé-ouvert au ré-open).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajoute assigned_staff à la requête + colonne Assigné (avatar initiales, couleur par tech) ;
issue_type renommé Type→Département. Distingue les tickets quasi identiques (ex. 3 Cancellation).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- TicketCard : badge Département (issue_type) + technicien (assigned_staff) → distingue les
tickets quasi identiques (ex. 3 « Cancellation » = Support/Facturation/Rétention).
- migrate_tickets.py : importe assigned_staff (ticket.assign_to → nom du staff) — le rattrapage
ne le posait pas. Backfill des ~7,5k tickets assignés en F mais sans technicien côté ERPNext.
- logToLinkedTicket : direction Received si l'expéditeur = adresse destinataire du ticket
(une réponse depuis un domaine interne @targo n'est plus mal classée « Sent »).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hub (conversation.js) : getOrCreateConvForIssue (lie une Issue à une conv email) + emailTicket
(envoie dans un fil Gmail, sujet [Ticket #ISS-…] + lien direct, journalise Sent Communication).
Route POST /conversations/ticket-email (gaté Authentik). La réponse du client re-thread par le tag
sujet (TICKET_REF_RX) OU le threadId → logToLinkedTicket (déjà en place) → Communication Received
sur l'Issue → visible au panneau. notifyCustomer mémorise désormais conv.threadId au 1er envoi.
UI (IssueDetail) : bouton « Envoyer par courriel » + dialogue (À/Cc/message), « Répondre » scindé
en « Note interne » (Communication) vs courriel. Lien direct = URL du ticket.
Testé : envoi réel à louis@targo.ca sur ISS-0000250013 (channel:email, Sent Communication OK).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- richHtml() (sanitize.js) : décode entités osTicket (é, ', double-échappées &#039;)
puis DOMPurify → plus de « codes html » dans les détails.
- IssueDetail : fusionne Échanges (Communication) + Fil (Comment) en UN fil chronologique
en bulles (notes internes en ambre) ; aère la mise en page.
- Bandeau Client cherchable par nom, association/dissociation 1 clic (comme une conversation)
— le ticket portait déjà .customer mais aucun champ pour le voir/lier. Résout le cas voicemail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JSX collapses the newline between {price} and <sup>.cents</sup>; a Vue template
renders it as a visible space. Made the dollar amount and the <sup> cents
adjacent in Services/Telephone/Television price cards. Verified: $39.95/$25.00/$10.00.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Footer internal links were absolute anchors copied verbatim from React (correct
at root, but under /next they jumped out to the React site). Switched the 7
route links to RouterLink so the router base is applied; hash (#apropos/#contact)
and external (store.targo.ca) links left as-is. Verified: footer now links
/next/internet, /next/support, /next/accessibilite, etc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real .env stays untracked (matches apps/website). Fixes admin white-screen
(supabaseUrl is required) by making the build-time requirement explicit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pixel-identical port of the React marketing site: Tailwind config, HSL theme
tokens, fonts (Plus Jakarta Sans / Space Grotesk) and custom utilities copied
verbatim; shadcn-vue (reka-ui) components; all 33 routes ported. Served at
www.gigafibre.ca/next (noindex) behind nginx, ahead of replacing the React site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La 1re version cascadait compte résilié ⇒ TOUS services Annulé, ce qui contredit
la politique 2026-06-13 (statut SERVICE = vérité ; employés gardent internet gratuit
sur comptes terminés) et annulait ~842 services légitimes (dont 185k$/mo facturés
encore par F ou gratuits).
Raffinement (sync_services_incremental.py, marqueur RAFFINÉ 2026-07-06) :
- annul_ghost(compte, prix) : n'annule QUE le fantôme AVÉRÉ = compte résilié +
service PAYANT (prix>0) + F silencieux depuis >24 mois (MAX(invoice.date_orig)).
Préserve les gratuits (employés), tout ce que F facture encore, et le jamais-facturé
(ambigu → revue manuelle via /rapports/resilies-actifs).
- Phases B (création) + D (rafraîchissement) passent par annul_ghost.
- Résultat mesuré : 3013 fantômes avérés annulés, 842 réactivés/préservés,
fantômes avérés restants = 0.
Scoreboard : métrique renommée « actifs sur compte résilié — à revoir » (hub note +
libellé UI) puisque le résidu n'est plus un bug mais une liste de revue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scoreboard (console de réconciliation F ↔ ERPNext) :
- hub: legacy-sync.js scoreboard() + GET /legacy-sync/scoreboard — comptes bruts
par module (clients/lieux/abos/appareils/tickets/factures/paiements) + métrique
ghost_active_subscriptions. Devices = tabService Equipment (pas tabDevice).
Count F « abos actifs » exclut les comptes résiliés (comparaison pomme-à-pomme).
- ops: carte « Réconciliation par module » sur LegacySyncPage.vue.
Fix fantômes à la SOURCE (scripts/targo-sync/, jusqu'ici hors repo) :
- La vraie sync des services = scripts Python hôte (/opt/targo-sync/, cron horaire),
PAS le hub. svc_status() ignorait le statut du COMPTE → un compte résilié F
(status 3/4/5) dont les services restent status=1 ressortait « Actif » (F ne
cascade pas). Résultat : ~3855 abonnements fantômes recréés à chaque heure.
- sync_services_incremental.py: svc_status() rendu conscient du compte (force
'Annulé' si compte résilié) sur Phase B (création) + Phase D (rafraîchissement).
Rollout: dry-run {Actif→Annulé: 3855} → APPLY=1 → ghost=0 stable.
- Versionne aussi run.sh + sync_invoices_incremental.py + README (snapshot fidèle
de la prod ; ces scripts écrivent en base chaque heure et n'étaient pas suivis).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A (garde-fou immédiat, sans écriture) : resolveAudience exclut les clients dont le compte F
est résilié (fResiliatedAccountIds, caché 30 min) — évite d'inviter d'ex-clients tant que la
donnée n'est pas nettoyée (cf feedback_ghost_active_subscriptions). L'aperçu affiche « N
ex-client(s) résilié(s) exclus ».
Vérifié prod : « clients actifs » 7943→5943 (2663 exclus) ; « abonnement actif ≥40$ »
3102→2213 (1125 ex-clients exclus). 33/33 tests. Déployé hub.
Ne corrige PAS la donnée sous-jacente (abos 'Actif' fantômes + MRR gonflé) ni la logique de
sync — voir tâche #25 (dry-run prêt : 3873 abos / 2663 clients / ~150K$ MRR fantôme).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Le champ « Total mensuel minimum » est désactivé tant que « Abonnement actif » n'est pas
coché (le montant vient des abonnements actifs) ; il se remet à 0 si on décoche.
- L'aperçu d'audience liste maintenant les comptes SANS courriel (rattachables) avec un lien
vers chaque fiche client (/clients/:id) → permet de valider/ajouter le courriel manquant.
resolveAudience renvoie `no_email:[{customer_id,name}]` (plafonné 300) ; l'endpoint le transmet.
Vérifié prod : filtre abonnement actif + ≥40$/mois → 3102 destinataires · 284 sans courriel
(liste des 284 renvoyée avec noms + fiches). Champ montant désactivé par défaut, activé au toggle.
33/33 tests unitaires. Déployé hub + SPA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ajout au sélecteur d'audience (mode masse) :
- Type de client : Résidentiel (customer_type=Individual) / Commercial (Company) / Tous.
- Total mensuel minimum ($/mois) : somme des Service Subscription ACTIFS (monthly_price)
par client ≥ seuil. resolveAudience agrège les abonnements actifs quand active_sub OU
min_monthly>0 ; le total est renvoyé par destinataire (monthly).
Vérifié sur données prod réelles : Résidentiel 7099, Commercial 928, total mensuel ≥50$ =
2739 clients (montants agrégés réels). Champs bien peuplés (contrairement à territoire, retiré).
32/32 tests unitaires. Déployé hub + SPA. Le blast réel reste gaté.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Couleurs restreintes à la charte TARGO (noir/vert/blanc/gris + bleu en haut) sur le
courriel ET la page RSVP : bandeau supérieur bleu #1a86c7, accents/CTA verts, texte
noir/gris, fond blanc, programme en pastilles vert/bleu/gris. Retrait orange/jaune/rose/rouge.
- Admin : le téléphone saisi s'affiche sous le courriel (n'apparaissait pas avant).
- Hub : capture de l'adresse IP du visiteur à l'inscription (x-forwarded-for → socket).
- CSV enrichi = TOUTES les infos : client, n° client, ID compte, nom au dossier, téléphone,
personnes, courriel, allergies, confiance, signaux, langue, IP, créé le, répondu le.
Déployé hub + SPA 2026-07-06. Vérifié prod : palette conforme, téléphone + IP stockés
(819 555-9999 / 96.125.192.22), test envoyé OK. 21/21 tests unitaires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Titre corrigé partout (courriel, page RSVP, admin) : « Fête Targo 20 ans » →
« Targo fête ses 20 ans » (FR) / « Targo turns 20! » (EN), tagline étoffée.
- Courriel refait façon flyer : bandeau confetti + ballons 🎈 + pastille « 20 ANS »,
programme en cercles colorés (vert/bleu/violet/rose/orange), encart « places limitées »,
ligne « Merci de votre confiance au fil des années 💚 », pied targo.ca.
Déployé hub 2026-07-06 (page RSVP live = « Targo fête ses 20 ans » ; test envoyé OK).
La page RSVP et le nom en admin viennent du hub — pas de rebuild SPA requis (le repli
cosmétique dans EventsPage suivra au prochain déploiement SPA). 21/21 tests unitaires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Suite aux demandes : bouton d'envoi test (courriel par défaut = utilisateur), auto-
rattachement par courriel/nom/téléphone, et confirmation « êtes-vous un client ? » par
recoupement — SANS autosuggest (aucune fuite de PII).
Hub lib/events.js :
- matchAndValidate({token,number,email,phone,name}) : recherche AVEUGLE de candidats par
courriel/téléphone/numéro (en parallèle), le nom corrobore. « confirmed » = ≥2 signaux
concordent sur le MÊME compte ; « probable » = 1 ; « none » = aucun. Jamais bloquant, ne
renvoie jamais de donnée client. Empêche un courriel deviné de mal rattacher.
- Formulaire RSVP : ajoute Nom (requis) + Téléphone (optionnel), n° client devient optionnel.
Message de remerciement adaptatif (« ✓ on vous a reconnu » si confirmed).
- POST /events/:id/send {test:true, test_emails[]} : envoi TEST via Mailjet/Gmail (réutilise
email.sendEmail/gmail.sendMessage + inviteEmail + rsvpLink). Envoi de masse reste gaté (400).
Ops EventsPage : badge de confiance (Confirmé/Probable/À vérifier + signaux en infobulle),
dialogue d'envoi test (canal + courriel défaut = usePermissions().userEmail), CSV enrichi
(téléphone + confiance). Filtres par niveau de confiance.
Vérifié : 21/21 tests unitaires hub (cross-validation + envoi test mailjet/gmail + masse gatée) ;
page Ops rend sans erreur, dialogue s'ouvre avec courriel pré-rempli.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
À la publication (vue Tournées), bouton « Notifier » → aperçu par tech
PUIS envoi. Chaque tech reçoit :
- UN lien token personnel (techFieldLink = app terrain, ses billets du
jour à répondre),
- la liste de ses interventions avec ADRESSE + flags cruciaux (AM/PM ·
URGENT · Appeler avant) détectés du titre/notes/priorité.
Hub (lib/roster.js) : composeDispatchNotifs(dates, techIds) + route
POST /roster/notify-dispatch (preview=compose sans envoi | envoi SMS via
twilio + Email via lib/email.js, best-effort par tech, retourne compte +
erreurs). jobFlags() détecte AM/PM/urgent/appeler-avant.
Client : api roster.notifyDispatch ; dialogue APERÇU (toggles SMS/Courriel
+ liste par tech + corps du message, alerte si pas de tel/courriel) →
« Envoyer ». Aperçu d'abord = pas d'envoi accidentel.
Vérifié : hub node --check OK ; client rend le bouton + dialogue + toggles,
0 erreur. ⚠️ Le PREVIEW/envoi nécessite le déploiement HUB (nouvelle
route) — GATÉ + sortant. Faire un test 1 tech avant usage réel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Suggérer peut servir de SIMULATION sur des jobs DÉJÀ dispatchés : nouveau
toggle « Ré-optimiser aussi les jobs déjà assignés » dans la config. Quand
activé, le solveur VRP reçoit AUSSI les noms des jobs assignés de la
fenêtre (assignedNamesForWindow, depuis occByTechDay) en plus du pool →
repropose une répartition de TOUT (existant + nouveaux jobs ajoutés).
Force le solveur (l'heuristique locale n'a que le pool en mémoire).
Aperçu seulement — rien n'est écrit avant « Appliquer » (qui réassigne).
Vérifié : toggle rendu dans la config Suggérer, 0 nouvelle erreur. Le
résultat de ré-optimisation (solveur roster-solver) est à confirmer en
prod (comme les autres runs solveur).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cause du « on ne voit qu'une partie du titre, aucun autre détail » :
openJobDetail lisait la forme BLOC (b.customer/skill/detail/legacy_id)
mais reçoit un job BRUT du pool quand ouvert depuis une goutte/carrousel
(customer_name/required_skill/legacy_detail/legacy_ticket_id) → tous les
champs vides sauf le titre. Corrigé : mappe les DEUX formes (client,
compétence, adresse, description, n° ticket, date) + charge le fil du
billet via le bon id → détails + commentaires s'affichent.
- Titre : plus tronqué (ellipsis → retour à la ligne, titre complet).
- Assignation DEPUIS le volet : sélecteur « Assigner à… / Réassigner… »
(jdAssignTech) ; si d'autres tâches à la MÊME adresse/client/jour →
propose de les assigner au même tech (réutilise assignNames +
siblingJobs, comme la grille).
Vérifié live (goutte carrousel, ticket #253649) : titre complet non
tronqué, méta = compétence tv + adresse 44 Carre Morrison + dept + date,
section commentaires + sélecteur d'assignation présents ; 0 erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retours terrain sur les modales de détail job (carte Tournées) :
- Volet détail 1 job : la CARTE (jd-map + tracé GPS) n'est pas requise pour
lire le ticket → repliée par défaut derrière « Voir la carte / tracé
GPS » (init à la demande, pas au chargement). Détails simples d'abord +
champ « Durée estimée » ajustable (comme « Suggérer ») qui persiste
duration_h (+ override de session). Titre déjà complet.
- Popups « liste des jobs à cette adresse » débordaient hors écran sans
scroll : maxWidth 260 + overflow-wrap + max-height 44vh scroll (carte
jour du panneau À assigner ET popup groupée RouteMap).
Vérifié live : volet 1 job → carte masquée par défaut, bouton déplie +
init canvas OK, champ durée = 2.25h ajustable ; 0 nouvelle erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Détection auto AM/PM : les tickets legacy « Note AM » / « Note PM »
(importés tels quels) sont détectés (detectAmpm : titre + note, tokens
AM/PM / avant-midi / après-midi, null si ambigu) et la contrainte
horaire est PRÉ-APPLIQUÉE dans « Suggérer » (suggestDlg.jobTime seedé à
l'ouverture, marqueur ⚡ auto_awesome sur la puce, modifiable). Le
solveur respecte alors la fenêtre 8-12 / 12-16.
2. Fenêtre RDV « 3 dispos du client » : chaque créneau proposé a un
sélecteur AM/PM (avant-midi/après-midi) qui règle l'heure représentative
(AM=08:00, PM=13:00), heure exacte toujours ajustable. Aligné sur AM/PM
du dispatch.
Vérifié live : Suggérer affiche auto_awesome + puce AM pré-cochée (jobs
« Note AM ») ; RDV → clic PM règle 13:00, AM règle 08:00 ; 0 nouvelle
erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#3 Regroupement par adresse : routeUnassignedPins groupe les jobs non
assignés d'une même adresse en UNE pastille portant les icônes des types
de jobs (distinctes, max 3) + le TEMPS TOTAL + un compteur ; couleur =
priorité la plus haute. Clic (tap mobile) sur une adresse à >1 job →
popup listant chaque job (sujet + durée), chaque ligne ouvre le détail →
plus besoin de cliquer chaque ticket, info visible d'un coup d'œil.
#4 Chevauchement : les gouttes non assignées rejoignent recluster()/fan()
(déjà éprouvé sur les arrêts assignés) → les pastilles proches s'éclatent
en éventail pour rester distinguables/atteignables. (Le regroupement par
adresse réduit déjà le fouillis ; le fan gère les adresses proches ≠.)
Vérifié par injection du markup (impossible de rendre la carte en dev,
jeu sans coordonnées) : pastille groupée = horizontale 92×22, 2 icônes +
compteur + « 45min » visibles ; popup = lignes cliquables. Compile OK,
0 nouvelle erreur. Rendu carte à confirmer en prod (jobs géocodés).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retours terrain :
1. « Ramassage d'équipement » = simple collecte chez le client → job court.
Détection (isPickup : titre/type ~ /ramm?ass|récupération/) → durée
plafonnée à 5 min : jobDur(pickup)=5/60 (maths d'occupation/Suggérer) +
capPickups() sur les 3 chargements du pool (est_min affiché=5). Évite de
surallouer le temps même si l'est_min du ticket dit plus long.
2. Icône de visite GÉNÉRIQUE (type non reconnu) : `bolt` (éclair, peu
parlant) → `handyman` (technicien + outils). Fallback de skillIcon →
s'applique partout (carte markerIcon + listes skillSym).
Vérifié live (données réelles) : ticket « Rammassage Équipements #253602 »
(St-Anicet) affiche ≈ 5min ; carrousel : plus de bolt, handyman présent
pour le job générique ; 0 nouvelle erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sur suggestion de l'user (visuel maison+fibre) : l'icône « installation »
passe de construction/tools-ladder à `router` (le routeur/ONT posé au
domicile) — plus parlant pour une installation fibre. Appliqué PARTOUT via
les 2 fonctions d'icône : markerIcon (carte) + skillSym (carrousel,
kanban, blocs jour/semaine). Ligature material `router` → rend aussi bien
dans le <span material-icons> des marqueurs carte que dans les q-icon.
(Le glyphe custom maison+fibre est un SVG → impossible sur les marqueurs
carte en DOM brut ; router = meilleur équivalent material intégré.)
Vérifié live : carrousel affiche router pour les 8 jobs d'install (à côté
de build/live_tv/bolt), 0 nouvelle erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Les jobs non assignés étaient de simples gouttes oranges SANS symbole,
alors que les assignés ont une pastille horizontale avec icône du type de
job (📞/🔧/⚡) + n°. Uniformisé en RÉUTILISANT le rendu éprouvé des arrêts
assignés (≠ tentative q-icon/SVG précédente qui rendait vertical/vide) :
- RouteMap.renderPins : pastille `.rm-pill` HORIZONTALE + icône via
<span class="material-icons"> (ligature material fiable), couleur =
priorité (orange = non assigné), SANS numéro d'ordre. Fini la goutte.
- routeUnassignedPins fournit `icon: markerIcon(required_skill||service_type)`
(markerIcon = ligatures material, celles qui s'affichent hors q-icon).
Vérifié : le markup exact injecté dans la page rend une pastille
HORIZONTALE 30×22 avec glyphe material 14px visible (police « Material
Icons ») — plus le bug vertical/sans-icône. Rendu carte identique à celui
des pastilles assignées (mêmes classes/mécanisme). À confirmer à l'œil en
prod (dev sans jobs géocodés → 0 marqueur).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Groupes de techs enregistrés (Suggérer) : sélection manuelle de techs
sauvée comme groupe nommé réutilisable (ex. Installateurs, Épisseurs) —
utile quand la compétence déduite du job est imprécise. Persisté par
utilisateur (useUserPrefs 'techGroups', suit l'usager entre appareils).
Chips « Groupes : » (clic = appliquer, ✕ = supprimer) + bouton
« Enregistrer » (nomme la sélection courante) + chip « Tout
désélectionner ». Complète le sélecteur par compétence existant.
2. Vue Tournées — clic sur un chip technicien = ISOLER sa tournée sur la
carte (masquer les autres) pour voir une route précise ; re-clic sur le
tech déjà seul = tout réafficher ; chip « Tout afficher » si masqués.
(Avant : le clic masquait/affichait un tech à la fois — inversé.)
Vérifié live : groupes save→apply→delete + « Tout désélectionner »
(13→0) OK ; 0 nouvelle erreur. (Les chips de tournée n'apparaissent
qu'avec des jobs géocodés — comportement solo vérifié par la logique,
à confirmer à l'œil en prod.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Les jobs ASSIGNÉS (marqueurs carte, HTML brut) utilisaient markerIcon =
ligatures material génériques (install→construction, monteur→camion
générique), alors que les jobs NON assignés (carrousel, blocs, gouttes)
utilisent skillSym = icônes SVG custom (échelle+outils, camion-nacelle,
casque). Résultat : même compétence, icône différente selon jour passé
(assigné) vs à venir (non assigné). Unifié sur les SVG custom :
- RouteMap : les marqueurs (arrêts assignés ET gouttes non assignées)
rendent l'icône via un VRAI <q-icon> (h+render) → gère les SVG custom
skillSym comme le reste de l'app ; repli material-icons si échec ;
démontage propre dans clearMarkers/clearPins (pas de fuite).
- PlanificationPage : les arrêts (Tournées + revue Suggérer) passent
skillSym(skill) au lieu de markerIcon ; markerIcon retiré (mort).
⚠️ Rendu carte NON vérifiable en préview (jeu dev sans coordonnées → 0
marqueur). Compile OK, carrousel (25 chips) inchangé, 0 nouvelle erreur.
À confirmer à l'œil en prod (jobs géocodés). Reste : unifier la FORME des
puces (chip/pill/carte) selon contexte — passe design dédiée.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Retours terrain :
1. « SLA dépassé » / en-retard se basait sur la CRÉATION du ticket
(jobSlaBadge → creation), ce qui marque en retard des jobs planifiés
plus tard (le client peut demander une visite quelques jours après
l'incident). Désormais l'échéance = la DATE DE VISITE PRÉVUE
(scheduled_date) : badge « En retard » seulement si scheduled_date <
aujourd'hui ; futur/aujourd'hui = à l'heure. Repli SLA-création
uniquement pour les jobs SANS date planifiée (backlog). (Le dispatch
utilisait déjà scheduled_date pour le périmètre — inchangé.)
2. Le carrousel « N à répartir » (Tournées) est plus long à scanner que la
liste par jour du legacy. La pastille « N à répartir » est maintenant
cliquable → ouvre la liste VERTICALE des jobs de ce jour (panneau
« Jobs à assigner » via openDayInPanel, filtré sur le jour) avec
recherche + chips type/compétence + tri par ville. Meilleure lecture,
surtout sur mobile.
Vérifié live (Tournées, 06/07) : clic « à répartir » ouvre le panneau
filtré (25 jobs du jour) ; 0 nouvelle erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le correctif précédent ne couvrait que l'entrée Tournées. Les jobs en
retard (scheduled_date < fenêtre) ou sans date étaient encore balayés sur
le jour choisi via d'autres chemins (bouton SUGGÉRER hors Tournées,
solveur ⚡ Optimiser). Corrigé À LA SOURCE + au placement :
- suggestJobs() (source unique — utilisée aussi par le solveur qui reçoit
job_names) : exclut désormais les jobs hors fenêtre (retard/sans date/
autre jour) SAUF si l'utilisateur coche « ⏰ en retard » / « Sans date »
ou fait une sélection lasso. Sélectionner un seul jour ne dispatche donc
QUE les jobs dus ce jour-là.
- buildSuggestion() : la branche fourre-tout `else cand = win` ne place
plus les retards/sans-date automatiquement — même garde (lasso ou chips
explicites) en défense.
Vérifié live (Tournées, 06/07) : plan de 25 jobs, tous sur « 07-06 »
(aucune date antérieure), 0 nouvelle erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deux retours terrain :
1. Jobs « en retard » (date < jour sélectionné) apparaissaient dans la
répartition auto. Cause : en vue Tournées, suggestWindow suivait les
chips de date du POOL, pas le jour choisi dans la bande → buildSuggestion
balayait les jobs antérieurs sur le jour (branche `else cand = win`).
Fix : en vue Tournées, `suggestWindow = [routesDay]` (la bande de dates
fait autorité) → seuls les jobs DUS ce jour-là sont répartis, jamais les
retards. (Le balayage des retards reste dispo hors Tournées via les chips.)
2. Revue « Répartition suggérée » trop dense sur mobile, lignes rognées
sans défilement. Fix : dialogue MAXIMISÉ <sm + chaque rangée job
(.suggest-entry) défile horizontalement (nowrap + overflow-x, enfants
non rétrécis, sujet borné) → toute la ligne (badges/AM-PM/actions)
accessible au swipe.
Vérifié live 375px : dialogue plein écran ; 25/25 rangées défilent
(scrollW ~698 > clientW 341) ; plan bâti sur le jour choisi ; 0 nouvelle
erreur console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sur la carte Tournées, les jobs NON assignés (props.pins) étaient de
simples gouttes oranges sans icône ni gestion du chevauchement, alors que
les arrêts ASSIGNÉS sont des pastilles round-rect (icône compétence + n°)
qui s'éclatent en éventail au survol quand elles se chevauchent
(recluster/fan). Uniformisé :
- renderPins() rend la MÊME pastille .rm-pill (icône de compétence via
pin.icon = skillSym), variante « non assigné » (.rm-pill-un : bordure
pointillée + halo orange), avec hover onEnter/onLeave.
- recluster() opère désormais sur stopMk.concat(pinMk) → arrêts et jobs
non assignés participent au même éclatement au chevauchement.
- routeUnassignedPins fournit `icon` (skillSym du required_skill/service).
⚠️ Visuel (pastille + éventail) NON vérifiable en préview locale : le jeu
de données dev n'a pas de coordonnées sur les jobs → 0 pin rendu. Code =
miroir fidèle du rendu d'arrêt éprouvé ; map s'initialise, 0 nouvelle
erreur console. À vérifier à l'œil en prod (adresses réelles géocodées).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Suite au carrousel : les chips de filtre de la bande « à répartir »
pilotent maintenant AUSSI « Suggérer » (« ce que je filtre = ce que je
répartis »).
- Chips PRIORITÉ (normalisée high/medium/low, via POOL_PRIOS) ajoutés à
côté des secteurs ; filtrent la liste + la carte (routeDayUnassignedView
= jour + compétence + secteur + priorité). Affichés seulement si >1
priorité présente (comme les secteurs).
- suggestJobs() : en vue Tournées, le périmètre = routeDayUnassignedView
EXACTEMENT (corrige l'incohérence : avant, les chips secteur étaient
ignorés par Suggérer alors que l'infobulle disait « du secteur »).
- openSuggest() : pré-sélection des techs par COMPÉTENCE — parmi les
disponibles (quart ce jour), ceux dont t.skills couvre un required_skill
des jobs du périmètre ; repli sur tous les dispos si aucun match.
Toujours modifiable (le dialogue liste les 56 techs, cochables).
Vérifié live (06/07) : secteur Huntingdon → vue 4 jobs → Suggérer affiche
« 4 job » (avant : ignorait le secteur) + 13/56 techs pré-cochés
(compétents+dispos, reste modifiable). 0 nouvelle erreur console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La bande « N à répartir » sous le sélecteur de jour (vue Tournées)
retournait à la ligne sur 2 rangées (max-height 70px, scroll vertical) et
PLAFONNAIT à 16 jobs (« +N de plus » cachait le reste). Passée en
carrousel horizontal 1 rangée (flex nowrap + overflow-x auto, scroll-snap,
barre fine) qui montre TOUS les jobs du jour — compact au-dessus de la
carte, tactile/tablette, aligné avec la bande de dates au-dessus.
Vérifié live (Tournées, 06/07 = 25 à répartir) : 25 chips rendus (fini le
plafond 16), 1 rangée nowrap, scrollWidth 5870 > 1214 = défile bien.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La section Tickets de la fiche utilisait un q-table dont le mode carte
mobile empilait des valeurs non étiquetées (« 0 », n°, sujet, avatars,
date…) = archaïque et peu lisible (cf. capture agent terrain). Nouveau
composant components/customer/TicketCard.vue : sujet en évidence + badges
statut/priorité colorés (édition inline conservée via InlineField) + n°
(legacy ou ISS) + date + avatars ouvreur/assigné empilés ; rangée
responsive (flex-wrap) lisible sur mobile ET desktop. Remplace le
DataTable dans la section Tickets ; imports morts retirés (ticketCols,
ticketStatusClass, priorityClass, staffColor, staffInitials).
Vérifié live (C-LPB4) : 5 cartes, sujet + Open/Medium + #ISS-… + date,
aucun élément > viewport à 375px, rendu propre à 1280px, 0 erreur.
Note : la réutilisation d'icônes de compétences dans les tickets est
reportée — les Issues (surtout legacy) n'ont pas de signal compétence
fiable (0 Dispatch Job correspondant) ; à faire quand un lien
ticket→compétence existe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le panneau de détail (DetailModal) était un panneau droit fixe 600px/90vw
et la carte réseau d'EquipmentDetail (.topo-row = 4 nœuds fixes 90px + 3
liens ≈528px) débordait/rognait sur mobile (nœud « Internet » coupé, IP/
onglets tronqués). DetailModal : sur <sm → dialogue maximisé pleine
largeur (position bottom) au lieu du panneau droit ; desktop inchangé
(600px droite). EquipmentDetail @<600px : nœuds topo flex (largeur auto),
liens 18px, dv-grid 2 col, dv-row2 en colonne, diag-grid 1 col.
Vérifié live (C-LPB4, EQP-0000011366) : 375px → carte pleine largeur, 0
débordement horizontal (docScrollW=375), 4 nœuds topo visibles
(Internet→OLT→Modem→17 app.), 0 élément > viewport, 0 erreur ; 1280px →
panneau droit 600px inchangé.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T7 découpage géants — sort le « lieu de service » (en-tête adresse +
DeviceStrip + abonnements mensuels/annuels drag-drop + tickets) dans
components/customer/LocationCard.vue. Les sélecteurs/actions stateful
(locSubs*/toggle*/onSub*/section*) restent liés aux instances de
composables du parent et sont passés en props-fonctions homonymes → le
template les résout inchangé, comportement identique (mêmes closures,
même invalidation de cache useSubscriptionGroups/useSubscriptionActions).
Helpers purs (formatMoney/isRebate/…) et usePermissions importés dans
l'enfant. Styles loc-*/sub-* copiés (scoped). ClientDetailPage -242 l. ;
imports morts retirés (DeviceStrip/locStatusClass/isRebate/subMainLabel/
sectionTotal/annualPrice/locInlineFields).
Vérifié en local sur hub LIVE (C-LPB4) : rendu pixel-identique (carte
691 rue des hirondelles, DeviceStrip live acs-online, ABONNEMENTS 4 =
INTERNET 3 / RABAIS 1, total 0,00 $/m), toggle de section fonctionne via
la prop (INTERNET 0→3 rangées, chevron expand_more), 0 erreur console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T7 découpage géants — sort le bandeau d'appareils (chips ONT/routeur +
menu Ajouter) dans components/customer/DeviceStrip.vue. Consomme le
composable singleton useDeviceStatus (couche données ACS/OLT partagée) ;
les helpers de présentation propres aux appareils (signalColor,
formatTimeAgo, doReboot, doRefreshParams) déménagent DANS le composant
(utilisés nulle part ailleurs). Le parent émet open-device + add-*.
ClientDetailPage -108 lignes ; imports/destructures device retirés.
Vérifié en local sur hub LIVE (C-LPB4) : chip vert + point acs-online
réel, tooltip ACS complet (SN TPLGC4160688, OLT oltRem03 1/3/0, En ligne
il y a 4m, Fibre Up, 17 clients WiFi, FW/WAN IP/SSID), 0 warning console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
É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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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'é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>
- 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>
Root cause of the bleeding numbers: Mapbox renders ALL circles then ALL
number-symbols in separate passes, so a number can never sit at its
disc's z-level — a hidden pin's number floats over a visible neighbor.
Sort keys can't fix cross-layer stacking.
Fix: stops + homes are now HTML mapboxgl.Marker elements — the number
is a text node INSIDE the colored disc (one element, same level →
correct occlusion, no orphan numbers). Routes stay GL line layers.
- Hover a pin → it scales up (.hot) and its whole route comes to front
(GL rm-line-a filter + .route-hot on same-rid markers).
- Overlapping pins EXPLODE on hover: recluster() unions markers within
24px (projected), computes a radial fan offset per member; entering
any cluster member translates all members' discs outward (CSS
transition) so all are readable/clickable; collapses 200ms after
leave; clusters recompute on moveend, collapse on movestart.
Verified: 43 numbered discs + 16 home markers; an 8-pin stack near
Huntingdon fans out on hover into individually readable pins; distinct
golden-angle colors preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Overlap fix: stop circles get circle-sort-key (k) and numbers get
symbol-sort-key (-k) with text-allow-overlap:false — the TOP circle and
the DISPLAYED number are now the same feature; the underneath pin's
number is culled instead of bleeding over its neighbor.
Hover-to-front: three "active" layers (line/stops/numbers, thicker line,
all numbers shown) sit above everything, filtered by rid; mousemove on a
route/stop (or hovering a tech chip / legend item) calls setActive(rid)
→ the whole tournée pops to the foreground; cleared 160ms after leave.
Exposed setActive() alongside fitTo/fitAll. Benefits both maps (review +
Tournées) since it's the shared RouteMap.
Verified: Houssam's magenta route renders on top on chip hover; no more
double numbers at overlapping stops.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Tournées tab: removable/addable TECH CHIPS above the map — each chip
in the tech's color with a job-count badge; click toggles the route
on/off (colors stay stable). Route colors switched from a 12-color
fixed palette to golden-angle HSL (137.5°) — consecutive techs always
get highly distinguishable hues, unlimited count.
- 🖥 SANS DÉPLACEMENT: per-job persistent flag (hub store data/
job-remote.json + POST /roster/job-remote; buildUnassigned sets
j.remote). optimizePlan EXCLUDES remote jobs from routing and address
pinning (e.g. "Configuration de boîtier tel" doable via ACS — nobody
travels) → returned in a remote[] bucket, shown as a "🖥 Sans
déplacement" group in the review, assignable to an agent. Toggle
button (computer icon) on every review entry. Verified: "Boitier
ouvert | Lac des pins" flagged → excluded from routes, then reverted.
- 📦 GROUPER EN PROJET: same-address entries (🔗) now open a menu →
"Grouper en projet" sets ERPNext parent_job on the shorter tasks
(parent = longest), POST /roster/job-group. Plugs into the existing
parent-child pool sort ("Groupe (parent-enfant)").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P4 — the FULL optimize orchestration now lives in the hub
(POST /roster/optimize-plan): pool (buildUnassigned), day bucketing
(dated-in-window → its day; overdue/undated → day 1 with spillover to
next day if unplaced), vehicles from real shift windows (assignments ×
templates) or assumed 8-16 weekdays (assumed_shift map returned; never
weekends; absences excluded), existing load (occupancyByTechDay) reduces
capacity, origins = home else TARGO depot, priorities from j.priority,
per-job AM/PM windows + duration overrides accepted, OSRM matrix +
OR-Tools solve per day (shared solveVrp, also used by /optimize-routes).
Reusable by the morning auto-plan cron and the field app.
Address rules (user cases baked in, hub-side):
- SAME ADDRESS = ONE STOP: pool jobs sharing an address key (~10m coords
or normalized address) are merged into one solver node (service times
summed) → same tech, single stop. Verified: install + TV boîtier of
the same customer (Cazaville) grouped on Stéphane, 🔗 badges.
- COORDINATION: a pool job at the address of an ALREADY-ASSIGNED job
that day is pinned onto THAT tech (they're going anyway) — verified:
"Réinstallation #253028" pinned to Frédérique who already has the
install there; capable flag kept honest (⚠ if skill missing).
SPA: optimizeSuggestion → hub-first (dates/techs/opts/dur_overrides/
job_windows), entries enriched client-side (lvl/eff/noShift from
assumed_shift); local orchestration kept as emergency fallback. Review
shows 🔗 teal (coordonné avec déjà-assigné) / 🔗 indigo (N jobs même
adresse = 1 arrêt); map merges same-coord stops into one numbered pin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
techOrigin(techId): a tech's departure point = their home if set, else
the TARGO office (depot from roster policy) — the stated default. Wired
into ALL 9 origin sites: greedy proximity, VRP vehicles (solver now
optimizes from the office), techsForJob/Selection ranking, review
nnOrder + suggestRoutes + loadLegTimes, and the Tournées tab.
UI: origin button on each tech — 🏠 (teal) when home set, 🏢 (grey) for
office-default — in the review header AND the Tournées legend; click
opens the existing home map picker (GPS position / search / map click).
The first-leg line now reads "🏢 bureau TARGO → 1er arrêt : X min" or
"🏠 domicile → …" (legsByKey carries the origin kind).
Verified: prod policy has the depot (Sainte-Clotilde) and 1 home → all
legends show 🏢; review shows real OSRM office→first-stop times
(21/43/29/26 min); pin opens "Domicile — Anthony Dion" picker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P2 — extract RouteMap.vue (reusable route map): colored per-tech routes
(instant straight lines → real OSRM geometry via hub, module-level
cache), numbered stops with popups, home markers, auto-fit, 'metrics'
emit (real km/min for legends), exposed fitTo(id)/fitAll(). The suggest
review map now uses it — deleted initSuggestMap/refreshSuggestMap/
destroySuggestMap/loadRealRoutes (~90 lines) and the TDZ-prone map
watches; the component is fully reactive on suggestRoutes.
P3 — new "Tournées" board view (grid | day | routes toggle, persisted):
the selected day's REAL routes (assigned jobs from occupancy, ordered by
route_order/start), one color per tech, real OSRM traces + km/min in the
legend, click tech = zoom (fitTo), click stop = detail popup. Day chips
over the visible week. Answers "tournée de la journée sélectionnée, par
tech avec chacun une couleur et trajets optimisés dynamiquement".
Verified: Tournées tab (16 routes, real road-following geometry,
metrics 140.4km/148min etc.); dialog map still renders via RouteMap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>