- 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>
P1 — self-hosted OSRM replaces ALL paid Mapbox routing in the SPA:
- hub: POST /roster/osrm-route (geometry + total + per-leg km/min) and
/roster/osrm-table (durations/distances matrix, Mapbox-shaped) proxying
the local OSRM; osrmGet helper.
- SPA: loadRealRoutes (review map), kanban travel matrix, day-editor
matrix, day-map route geometry → all via hub OSRM. 0 paid
directions/matrix calls left (tiles + geocoding only).
Review usability (evaluate whether times fit):
- Click a job → the SAME job-detail dialog as the grid (address, ticket
thread, duration, team). openEntryDetail maps the pool job.
- Per-task time is editable (q-popup-edit on the "X.Xh"): updates the
plan live, survives Ré-optimiser (durOverride), persists duration_h
via patchJob (hub whitelist extended, 0-24h).
- Real travel time BETWEEN stops: one OSRM route per tech×day (cached)
→ "🚗 X min" rows between entries + "domicile → 1er arrêt";
haversine ≈ fallback when coords missing.
Verified: OSRM proxies live (route 7.8km/12min with legs 7+6min, table
3x3); review shows 10 real leg rows; detail dialog shows address;
duration popup opens/cancels cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Each tech group day now exposes the jobs ALREADY assigned (OPS
occupancy + legacy F field tickets) behind a "🔒 N déjà assigné(s) ·
cities" toggle — locked greyed rows (time, skill, subject, duration).
Header shows 📍 sector(s) where the tech already works, so the
dispatcher can adjust accordingly.
- Header shows the tech's skills IN PRIORITY ORDER (★ first + "+N"),
clickable → same drag-reorder skill editor (visible answer to
"ordonner les compétences depuis le résultat").
- cityOfOcc: heterogeneous legacy subjects ("Type | Ville | Client" vs
"Ville | Client") → picks the first city-looking pipe segment (no
digits, ≤4 words, no job stop-words), else address tail, else
" - Ville" suffix. Avoids garbage like "Bris de fibre" as a sector.
Verified: Simon Clot-Gagnon → 📍 Sherrington, 2 locked repair rows;
Anthony Dion → ★ installation · réparation · +4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaced the distance↔specialty slider (heavy) with 4 compact number
inputs: Spécialité (rank_weight), Heures sup (overtime_coef — now
tunable, was hardcoded), km/h (speed), Calcul s (max_seconds). Persisted
in solverOpts (localStorage), passed to the solver. Tooltips replace the
verbose inline hints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- default strategy = optimize → the FIRST Générer runs the VRP (real
techs, no more greedy placeholders on first attempt).
- occupation bar is now PER-DAY (worst day), so a 2-day window shows /8h
not /16h; overload shows in red.
- solver: each vehicle may go up to +20% overtime (≈120% cap, hard) but
time past the nominal shift is soft-penalized → overflow spreads to
techs still under 100% before anyone does overtime. Verified: 20 jobs /
2 techs → 9h+9h (balanced, ≤9.6h), overflow dropped; under capacity it
still concentrates (no forced equalization).
- priority medium flag = amber (yellow/orange) per request.
Verified live: default optimize on, 16 real techs / 0 placeholders,
per-day /8h occupation, none over, medium flag amber.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- shared priorityMeta/PRIORITY_LEVELS → 3 levels: Haute (RED) · Moyenne
(neutral grey) · Basse. Default/medium = grey outlined_flag. Aliases
urgent→high, normal→medium for old conversation data.
- conversations: priority endpoint accepts 'medium' (keeps old values for
back-compat); the flag menu now shows the 3 levels.
- jobs: dropped the parallel job-flags override store — the job flag now
writes the REAL ERPNext priority (low/medium/high) via patchJob/
updateJob, same source as the pool sheet (no more two mechanisms).
setEntryPriority reuses patchJob. Solver: high never dropped + served
early, low dropped first.
Verified: job flag + conversation flag both grey-by-default, menu Haute/
Moyenne/Basse, high=red.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The urgent flag was a red emoji shown red before being set — wrong.
Now reuses the exact Communications component: priorityMeta() + a
priority list (Urgent/Haute/Normale/Basse/Aucune) — grey outlined_flag
by default, colored only when set.
- SPA: per-job flag = q-btn(icon=priorityMeta(...).icon) + q-menu list,
same as ConversationPanel. setEntryPriority(name,lvl) (renamed to avoid
clashing with the existing pool setJobPriority(j,p)). medium (ERPNext
default) shows as grey/none.
- hub: job-flags store now holds a dispatch PRIORITY (urgent|high|normal|
low) overriding j.priority in buildUnassigned — needed because the
ERPNext job-priority field is whitelisted to low/medium/high (can't do
urgent), just like conversations keep their own priority.
Lesson applied: grep for an existing symbol before declaring (2nd
collision this session — techsForJob, setJobPriority).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Overload fix: each vehicle's window is reduced by the tech's already-
assigned load that day (shift_start += existing) — no more 20.8/16h
(verified: William 20.8→5.3/8h). Capacity is now real.
- No more "Tournée N / non casé" pseudo-techs: the optimizer uses ONLY
the selected techs; everything it can't place goes to ONE plain
"⚠️ Non assignés" bucket (weekend + capacity/skill overflow). Assign
them via the proximity+load ⇄ menu, or add techs.
- Not forced to fill every tech: unneeded techs are simply left empty
(day off) — the VRP no longer fake-distributes.
- Edit/reorder skills from the optimizer: ✏️ on each tech header opens
the same skill editor (drag-reorder chips + ★ + rank list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Like the omnichannel inbox urgent flag, but for jobs — a 🚩 that
persists and drives the optimizer.
- hub: durable job-flags store (data/job-flags.json) mirroring
job-levels — getJobFlags/setJobFlag + POST /roster/job-flag {name,
urgent}; buildUnassigned sets j.urgent; invalidatePool on write.
- SPA: 🚩 chip on each review entry (replaces the session ⚡). Optimistic
local toggle + persists via roster.setJobFlag; reflects j.urgent from
the pool. Flagged jobs → priority_boost (never dropped) + urgent_weight
(served early). Bumped urgent early-weight 6→10.
Verified: flag round-trips (set→urgent, revert→cleared) through the pool;
chips render (🚩/AM/PM).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>