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>
Each job's move menu in the optimize review now reuses techsForJob()
(capable → with-shift → nearest → least-loaded) instead of alphabetical,
showing per tech: distance (home→job km) and load (h/cap). Lets you drop
a "non casé" job onto the best nearby, least-busy capable tech — the
manual version of "move a job to free a tech's capacity". Works on
placeholder/unassigned entries too (same row).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Time-aware dispatch in the VRP optimizer:
- solver: per-job urgent_weight → SetCumulVarSoftUpperBound(idx, 0, w),
a soft cost on arrival time that pulls urgent jobs to the START of the
route (conditions the tech's day start). Verified: a far urgent job is
served first instead of last. AM/PM handled via existing tw_start/end.
- SPA: per-entry AM / PM / ⚡ chips (session map suggestDlg.jobTime,
survives re-solves) + a "Ré-optimiser" button. optimizeSuggestion maps
AM→[480,720], PM→[720,960] (hard window) and urgency (job priority OR
manual ⚡) → urgent_weight. Verified: chips render, toggle persists
across re-optimize, OSRM still engaged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Config UI (Optimiser mode): live settings — a distance↔specialty
slider (rank_weight 0-10), fallback speed km/h, and solve-time budget.
Persisted to localStorage, passed to the solver per run → tune the
curve without redeploying.
- Urgent jobs prioritized: each job's priority maps to priority_boost
(drop penalty) — urgent/high never dropped, low priority dropped first
when capacity is tight.
SPA-only (solver already accepts rank_weight/speed_kmh/max_seconds/
priority_boost). Verified: settings panel renders, optimize runs
end-to-end (16 techs, OSRM).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Skill capability stays a hard filter, so a multi-skill tech (e.g. an
installer who also has the repair skill) can take those jobs. But the
specialist bias (skill-order rank) was too strong (6 virtual min/rank),
so a far repair-specialist beat a nearby installer-who-repairs. Lower
rank_weight to 2 → distance dominates; specialty only breaks near-ties.
Verified: repair job near a rank-3 multi-skill installer (4 min) vs a
far rank-0 specialist (14 min) — old weight picked the far specialist
(bug), new weight picks the nearby installer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The VRP « Optimiser » now treats each SELECTED tech as having a 8-16
shift (created at Publish via makeShifts) — not just those already
shifted. Weekday placeholders are folded back into their day so the
solver assigns them across all selected techs; weekend placeholders
stay (no auto weekend shift). Result: full multi-tech consolidation
instead of everything falling to placeholders — verified 34 jobs → 16
techs, tight routes (0.5–14.7 km each). Techs without a real shift show
« Créer quart » (their assumed 8h, created at Publish).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real vehicle-routing optimization behind the Suggérer UI, reusing our
existing OR-Tools solver service (no new stack). Fixes the greedy's
structural limits (sector-splitting, no global optimization).
- roster-solver: new route_solver.py (OR-Tools Routing / VRPTW).
Minimizes real travel; skills = hard filter (VehicleVar ∈ allowed∪{-1};
SetAllowedVehiclesForIndex has a broken Span typemap in ortools 9.15);
on-site service time within each tech's shift window; optional per-job
time windows; unfittable jobs left unassigned (drop penalty) instead of
infeasible; specialist bias via skill order (per-vehicle arc cost).
New POST /route endpoint. Dockerfile now COPYs route_solver.py.
Unit-tested: skills respected, sectors consolidated, edge cases safe.
- hub: POST /roster/optimize-routes → proxies to solver /route.
- ops SPA: "⚡ Optimiser" strategy. Greedy buckets jobs into days +
placeholders, then the solver re-optimizes each day (assignment +
routes) among shifted techs; result maps into the same review dialog
(occupation bars, route map, swap/merge reused). Graceful fallback to
greedy if the solver is unreachable (never drops jobs). shiftWindowMin
derives each vehicle's shift from templates.
Phase 2 (later): OSRM/Mapbox road-time matrix for exact travel times.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Skill order (not just level) now drives auto-dispatch: index 0 in a
tech's skills = their primary function. A job goes to the specialists
of that skill first, sparing less-specialized techs for other work
(e.g. Louis-Paul does repairs but his primary is sales → repair jobs
prefer the repair/install specialists).
- buildSuggestion: skillRank = t.skills.indexOf(reqSkill) (0 = primary);
score += skillRank * W.rank, tuned per strategy (enough/smart weight
specialty highest). Skill capability stays a hard filter.
- TagEditor (shared component): chips are now drag-reorderable
(vuedraggable, touch-friendly) via `sortable` prop; the 1st chip is
marked ★ primary. Reorder emits the reordered array → persists as the
ordered skills CSV (order round-trips; stored in custom `skills`
field, not Frappe _user_tags, so no alphabetical re-sort).
- Skill editor: enable sortable chips + explicit priority numbers
(1,2,3…) on the per-skill list, #1 highlighted, plus a hint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Overhaul of the Planification « Suggérer » (auto-dispatch) flow:
- fix: read latitude/longitude (hub pool field), not lat/lon — the
distance scoring was inert (NaN) so jobs scattered; they now cluster
by real route (haversine) with a same-city fallback
- skill = HARD filter: incapable techs are no longer candidates
(Josée-Anne no longer gets réparation/installation). Assign/move menus
list capable techs first and block the rest, unless no capable
alternative exists (avoids trapping an unassignable queue)
- placeholder queues for any no-shift day (not just weekend), grouped by
skill then geography into ~8h tournées; the owner is swappable to a
real tech, and two queues can be merged
- per-tech occupation bar in the review (existing load + suggested /
capacity, red on overload); "Créer quart 8-16" button when the
assigned tech has no shift that day (local, saved on Publier)
- fix: "N jobs sans coordonnées" badge vs locate mismatch — honest
message + open the manual map picker for address-less jobs; also sync
pool jobs (latitude/longitude) on manual save so the badge refreshes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Logique métier : c'est le job précis qui impose la compétence/niveau, pas une
caractéristique globale. Retrait du bouton + éditeur « Niveau requis par compétence »
(level_by_skill/level_by_type globaux) et du repli global dans le moteur → le niveau
vient UNIQUEMENT du job (required_level persistant). La gestion compétences+niveaux se
fait PAR TECHNICIEN dans le Suggérer via l'icône ✏️ = TagEditor (chips + ★), le MÊME
composant que le tableau timeline (Dispatch). Sous-compétences (ex. émondage) = simples
compétences/tags créables et assignables au tech.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Niveau requis PAR JOB persistant : store hub durable (job-levels.json) + endpoint
POST /roster/job-level + buildUnassigned enrichit required_level ; pastille « niv »
écrit via l'API (survit reload/session). NB : l'API Custom Field ERPNext v16 échoue
(IndexError) sur ce doctype custom → store hub (migratable en champ ERPNext plus tard).
- Lasso FREEFORM : tracé libre (souris + tactile) → polygone SVG + point-in-polygon sur
les pins/amas projetés (remplace le rectangle) + expansion des amas (getClusterLeaves).
- D v2 : carte des tournées = routes ROUTIÈRES réelles (Mapbox Directions par tech,
domicile→arrêts) + distance/temps RÉELS dans la légende (cache par signature ;
fallback segments droits + estimation haversine).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Sécurité : sanitize DOMPurify sur tout HTML entrant courriel/osTicket (v-html) →
ferme le vecteur XSS (ConversationPanel + IssueDetail). Nouveau src/utils/sanitize.js,
dompurify ajouté en dépendance directe.
- Fix : piège TDZ (watches de la carte des tournées placés avant les const suggestDlg/
suggestMapDay/suggestGroups) qui cassait le montage de PlanificationPage → déplacés
après les définitions.
- Finition : étiquette de fenêtre du dispatch auto « Aujourd'hui + Demain » (au lieu de l'ISO).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).
Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Comms: sélecteur d'expéditeur « De: » (défaut groupe Support TARGO) via resolveSendFrom + alias vérifiés
- Notifs: prefs de feeds PAR utilisateur (/conversations/notif-prefs) + cloche à bascules ; boot tooltip-ux (clic prioritaire + anti-empilement)
- Courriel: invitation à évaluer = modèle Unlayer éditable (transactional-rating-invite-*) ; test-send via Gmail + expansion {{rating}} ; logo TARGO auto-hébergé sur le magasin d'actifs du hub
- Planif: bloc « sans déplacement » (damier, début de quart, alerte si pas de quart), quart éditable dans l'éditeur de jour, icônes de compétence en vue jour (TV pour télé), clic cellule → éditeur, clic gauche lane → liste / clic droit → menu quart, lien ↗ ticket par job
- Tickets: défaut « Non fermés » + correction du filtre « Mes tickets » (owner)
- Inbox: poll Gmail 1 min + rafraîchir à la demande (poll-now)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Email posts now render at FULL content height (fitFrame cap raised 1800→20000 guard)
so there is no per-message internal scroll — the whole thread scrolls as one.
- The live draft mirror ("X rédige une réponse…") gets a close X (closeOtherDraft):
dismiss another agent's in-progress draft preview; reset on conversation switch so
a genuinely new draft re-appears.
Verified: a 60-paragraph email renders at 2681px (no inner scrollbar); the mirror
shows a close button that hides it on click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Email messages now size to their own content (fitEmailFrame): iframe sandbox gains
allow-same-origin (NO allow-scripts — scripts stay blocked, parent just measures
body.scrollHeight) and the height is set on @load + re-measured on expand. Removes
the fixed 300px frame and the "fill empty space" stretch (no more big whitespace).
msg-body is now v-if (renders on expand) so the measure runs while visible + lazier.
- Reply arrow in each CUSTOMER message header → opens + focuses the composer without
scrolling to the bottom button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sender identity on outbound:
- Replies/new emails now go out as e.g. "Gilles Drolet" <support@targo.ca> instead
of the generic alias name "Service TARGO". The name is derived from the agent's SSO
email (gilles.drolet@ -> "Gilles Drolet"); the ADDRESS stays the monitored alias so
replies still land in the shared inbox. Verified: Gmail honors the custom display
name on the send-as alias (sent From header = "Gilles Drolet <support@targo.ca>").
- gmail.sendFrom() exported; conversation.js agentSendFrom() builds the From from the
stamped msg.agent (replies) / x-authentik-email (new sends via email-new).
UI cleanup (space + clarity, Gmail-style):
- One thin separator line between messages; removed the rounded gray card box around
emails (.email-card flat), the email-card-head bar, and the blue expanded-head
highlight.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On mobile the conversation full-page used a fixed height + overflow:hidden, and
the reply q-editor had no max-height — a long body/signature grew it unbounded
and pushed the send button past the clipped bottom (user had to shorten the email).
- q-editor: max-height=40vh → content scrolls internally, toolbar + send row stay put.
- ConversationFullPage @media (max-width:700px): height auto + min-height 100dvh +
overflow visible → the page scrolls vertically instead of clipping.
Verified at 390x844 with a 27-line signature: editor caps at 40vh (overflow auto),
"Envoyer le courriel" stays in view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inbox identity/grouping (fixes mislabeled threads):
- customerName = real From display name, then a customer matched BY EMAIL,
then the raw address — never the AI's content-guessed name (a gilles@ relay
showed as "Sylvie Juteau").
- never group a thread by one of our own domains (support@targo.ca,
*@targointernet.com): re-ingested outbound was collapsing unrelated threads
into one mislabeled "Guylaine Gagnon" thread.
Refactor: queues/TYPES/QUEUE_OF/CAT centralized in lib/categories.js (were
drifting across conversation.js + inbox-triage.js; telephonie/television had
no matching triage type). Removed dead export findConversationByEmail.
Feat: addMessage stamps msg.agent on outbound replies -> per-agent stats.
Historique (/historique, HistoriquePage.vue): tournées par technicien (ALL
statuses incl. Completed/Cancelled — the board hid them), tech leaderboard,
inbox leaderboard. Hub: /dispatch/history, /dispatch/leaderboard,
/conversations/leaderboard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hub (lib/roster.js, vision.js, legacy-dispatch-sync.js, server.js) + Ops + pont legacy.
- Capacité par jour AM/PM (Soir = réserve garde/urgence, jamais offerte) calculée
client-side sur les techs visibles -> suit le filtre de compétence.
- Modèle de durée ADDITIF (caractéristiques, tableur inline) + auto-détection
DÉTERMINISTE par mots-clés (sans IA permanente) ; est_min branché sur capacité + pool.
- Capture terrain passive : endpoints publics /field (job/tech/checkpoint/ts/photo/
device/vision), tokens HMAC signés sans PII ; dérive actual_start/end. UI hébergée
public/field-app.html (liste/carte Mapbox/Street View/photo/scan MLKit->Gemini).
- Chrono job (start/finish), repositionnement carte (set-location), vue satellite,
Street View clic-droit, année devant les dates dues groupées.
- Sync techniciens : rapport de réconciliation 3 systèmes (staff legacy / Dispatch
Technician / groupe Authentik), application MANUELLE, zéro écriture Authentik (+11 fiches).
- vision.js : extractEquipment() réutilisable (marque/modèle/série/MAC/codes-barres).
- Pont legacy (ops_reassign.php) : désassignation reflétée, fermeture ticket, retour
au pool ; notification courriel à l'assignation.
Déployé sur le hub ; ce commit aligne le repo sur l'état en production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Panneau « Jobs à assigner » : tri/groupes par DATE DUE (≠ création) avec étiquettes
relatives (« Aujourd'hui », « MM-DD ⏰ en retard », future) + date due affichée sur chaque ligne
(overdue en orange). ASC/DESC place aujourd'hui dans l'ordre chronologique.
- Fermeture (unitaire + lot) : confirm via $q.dialog (au lieu de window.confirm) — propre + fiable.
- Carte → liste : clic sur un pin sélectionne + scrolle la ligne dans la liste + déplie son fil + flash visuel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Axe de temps (hdr-ruler 0..24) retiré des en-têtes de jour (inutile dans la grille ; encombrant).
- La bande d'occupation `.tl` ne stoppe plus la propagation (plus de @mousedown.stop/@click.stop) → le
clic/drag sur la cellule remonte à `<td>` → onCellClick (menu d'horaire) + onDown/onEnter (sélection) refonctionnent.
- L'éditeur de journée s'ouvre désormais en cliquant un BLOC de job (.tl-blk, @click.stop) — pas toute la
bande. Donc : clic sur un bloc coloré = éditer la tournée ; clic ailleurs dans la cellule = modifier l'horaire.
Le tooltip d'occupation (survol) reste actif. Curseur/hover sur les blocs cliquables.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Barre timeline : l'espace entre 2 jobs (le déplacement) est rendu en POINTILLÉS (au lieu du gris vide),
tooltip « 🚗 déplacement ». dayTravelSegs() = gaps entre packedDay[i].end et [i+1].start.
- Carte : clic sur un pin → POPUP avec détails du job (n°, sujet, heure, client) ; curseur main au survol.
- Liste : clic sur une ligne → recentre la carte (easeTo zoom 14) sur ce job, en plus d'ouvrir le détail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remplace la mini-image statique (segments à vol d'oiseau) par une carte Mapbox GL :
- Itinéraire ROUTIER réel via l'API Directions (geometries=geojson) tracé sur la carte (halo + ligne).
- Pins numérotés dans l'ordre de tournée (cercle coloré par compétence + numéro).
- Navigable : zoom molette + boutons NavigationControl (+/-), déplacement (pan), ajustée au territoire (fitBounds).
- Lifecycle : init à l'ouverture du dialogue (après anim + resize), refresh débouncé au réordonnancement
(re-trace l'itinéraire), destruction à la fermeture (pas de fuite). mapbox-gl chargé en CDN (comme le Dispatch).
- Avertissement « N sans coords » conservé. Validé : Directions OK (géométrie 392 pts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dernier recours quand l'adresse exacte est introuvable : placer le Service Location au CENTROÏDE
(rqa_addresses) de son code postal (préféré) sinon de sa ville → le job apparaît dans le bon secteur.
- Hub : applyAreaFallback() (CTE centroïdes CP/ville, index-friendly) + POST /address/conformity/apply-area.
Statut 'area', linked_address '≈ centre <CP/ville>'. Hors-QC/junk (absents de rqa_addresses) restent unmatched.
- Ops : carte stat « ≈ Secteur (CP/ville) » + bouton « ≈ Centre CP/ville (reste) » dans la page Conformité.
Exécuté : 317 placés (303 par code postal, 14 par ville) → unmatched 365 → 48 (Toronto/boîtes postales/junk).
État final : validated 16 257 · review 489 · area 317 · unmatched 48 → 99,7 % des services ont une position.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rend le mécanisme réutilisable (« faire de même pour tous les campings ») :
- Hub (address-conformity.js) : GET /address/conformity/campings (registre + nb lots par camping),
POST /campings (upsert {keyword,name,address,lat,lon} → applique direct), POST /campings/apply (réappliquer).
applyCampings() = UPDATE des lots (match ville normalisée) → géoloc fixe du camping.
- Ops (page Conformité adresses) : section « Campings — géoloc de remplacement fixe » : table du registre
(nom, adresse principale, GPS→Google Maps, nb lots) + formulaire d'ajout (nom/mot-clé/adresse/lat/lon)
qui ajoute ET applique, + bouton « réappliquer ». api/address.js : campingsList/Upsert/Apply.
→ Pour un nouveau camping : on saisit son adresse principale + GPS, tous ses lots pointent dessus (le tech
navigue au camping). Registre seedé : Lac des Pins, Dauphinais, SandySun, Frontière, Ensoleillé.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Minimap Mapbox Static ajoutée sous la timeline : pins numérotés dans l'ordre de tournée + tracé reliant
les arrêts, ajustée auto au territoire des jobs → on VÉRIFIE d'un coup d'œil que chaque arrêt tombe à la
bonne adresse (un pin mal placé = coord à corriger via Conformité adresses). Clic → carte interactive (Dispatch).
Indique « N sans coords (absent de la carte) » le cas échéant. Helper encodePolyline (precision 5) pour le tracé.
- Sélecteur de priorité retiré de chaque ligne (défaut « Moyenne » conservé en donnée, géré au Dispatch) → gain de place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plus besoin de re-chercher avec un processus complexe : une page liste les adresses de service non
conformes (review/unmatched) avec leur proposition AQ canonique, et permet de RÉSOUDRE une fois (persisté) :
- Approuver : la proposition AQ devient officielle (validated, coords RQA).
- Corriger : recherche AQ locale (rqa_addresses + fibre) → lier la bonne adresse.
- GPS : saisir/coller lat,long (relevé sur map.targointernet.com qui a la géoloc des unités de camping)
+ lien direct « voir sur la carte » par ligne.
- Rejeter : pas d'adresse civique (boîte postale/hors-QC) → 'no_address'.
Tri par type (camping / civique à corriger / à confirmer / non-adresse) + stats + recherche + pagination.
Backend : lib/address-conformity.js (GET stats|list|candidates, POST resolve) sur le Postgres LOCAL,
routé /address/conformity/* (server.js). Front : api/address.js + pages/AddressConformityPage.vue + route
/conformite-adresses + entrée nav « Conformité adresses » (icône MapPinned, requires view_settings).
État courant : validated 15 195 · review 1 366 · unmatched 550 (camping 540 / civique 333 / non-adresse 93).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pont (legacy-dispatch-sync.js) :
- Import des coordonnées par job via cascade : table legacy `delivery` (point de service exact,
JOIN ticket.delivery_id) > Service Location ERPNext > géocodage RQA > géocodage Mapbox.
Validation bornes Québec (coord()). Couverture 153/172 (89%).
- Géocodage RQA corrigé : retrait du générique de voie (Rue/Rang/Chemin absent de
odonyme_recompose_normal) + code postal non accolé au terme (sinon ilike ne matche jamais).
- Repli Mapbox geocoding pour rues trop récentes pour le RQA (MAPBOX_TOKEN).
- Backfill + UPGRADE : coords delivery écrasent des coords SL moins précises (jamais l'inverse).
- Comptabilité honnête : vérifie r.ok sur create/update (erp ne throw pas) → errors + error_samples.
- Verrou de sérialisation sync() : tick + runs manuels ne se chevauchent plus (frappe_pg).
- Subject tronqué à 140 (champ Data) → corrige CharacterLengthExceededError sur jobs sans SL.
- Observabilité : coord_src tally + error_samples dans le résumé.
Ops Planification (éditeur de journée) :
- travelBetween() consulte une matrice Mapbox Matrix chargée à l'ouverture (loadDayRoute) →
temps de trajet ROUTIERS RÉELS ; réordonnancement instantané sans nouvelle requête.
Repli haversine si Mapbox indispo. Indicateur 🚗 réel vs 📏 vol d'oiseau.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FIX réordonnancement : flèches ↑↓ (fiable) + drag-drop basé sur le DROP (au lieu du splice live jittery)
- durée éditable par job en MINUTES (pas de 5, best practice précision) → persistée via reorder-jobs (duration_h)
- temps de transport estimé entre 2 jobs (haversine sur coords Service Location, 40km/h + 5min) affiché entre les lignes
→ en attendant la géoloc live (Capacitor background-geolocation, noté pour plus tard)
- hub : occupancyByTechDay renvoie lat/lon par job ; reorder-jobs accepte duration_h ; total h en pied
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>