Commit Graph

514 Commits

Author SHA1 Message Date
louispaulb
86bd8080d2 fix(dispatch): optimizer respects existing load; drop pseudo-tech placeholders; edit skills from review
- 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>
2026-07-02 15:35:25 -04:00
louispaulb
7d1a5b067f feat(dispatch): persistent URGENT flag per job (SLA/commercial)
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>
2026-07-02 15:16:13 -04:00
louispaulb
8b2d7b54e9 feat(dispatch): pre-wired sites — install time ÷4 (Lac des pins/Camping/Domaine Dauphinais)
estimateForJob: an INSTALLATION whose location matches a pre-wired site
(Lac des pins, Camping, Domaine Dauphinais) takes ¼ of the normal time
(infra already in place). Applied at the estimator source → flows into
est_min everywhere (greedy duration, VRP service_min, occupancy, travel).
Floored at 15 min; adds a "site pré-câblé ×¼" label. Verified: Lac des
pins install 120→30 min; normal install unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:08:33 -04:00
louispaulb
2566976191 feat(dispatch): review job-move menu ranks techs by proximity + occupancy
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>
2026-07-02 15:05:05 -04:00
louispaulb
f5a7547c1f feat(dispatch): urgency → early-in-day + AM/PM windows per job
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>
2026-07-02 14:42:36 -04:00
louispaulb
3513f7e15f feat(dispatch): solver settings (distance↔specialty, speed, time) + urgent priority
- 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>
2026-07-02 14:03:11 -04:00
louispaulb
bc157588d1 tune(dispatch): VRP prioritizes distance over specialty (rank_weight 6→2)
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>
2026-07-02 13:53:46 -04:00
louispaulb
96929bed6f feat(dispatch): OSRM road-time matrix for the VRP optimizer (Phase 2)
Replace the haversine straight-line estimate with real road travel times
from a self-hosted OSRM (free/offline — Mapbox Matrix is billable).

- hub: osrmMatrix() calls OSRM /table for the job+home coords (node order
  matches route_solver: jobs then homes), returns a minutes matrix.
  Handles PARTIAL coords — OSRM for geolocated nodes, 0/neutral for the
  rest (matches the solver's own fallback), so a few tech homes without
  coords don't disable OSRM for the geolocated majority. /roster/
  optimize-routes injects body.matrix; graceful fallback to haversine if
  OSRM is unreachable or too few points.
- config: OSRM_URL (default http://osrm:5000).
- services/osrm/README.md: reproducible setup (Québec extract, MLD
  pipeline, osrm-routed on erpnext_erpnext network).

Deployed + verified: OSRM /table hit with the full coord set, no
fallback; optimizer routes on real road times.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:48:43 -04:00
louispaulb
5803fb3d0e feat(dispatch): optimizer assumes 8h shift for every selected tech
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>
2026-07-02 13:34:37 -04:00
louispaulb
a6508845a7 feat(dispatch): VRP route optimizer (OR-Tools) — "Optimiser" mode
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>
2026-07-02 11:24:08 -04:00
louispaulb
4591ef6169 feat(dispatch): skill PRIORITY ordering per tech (specialists first)
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>
2026-07-02 10:50:51 -04:00
louispaulb
f6e5128859 feat(dispatch): skill-driven auto-dispatch — route grouping, placeholder queues, shift check
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>
2026-07-02 10:37:40 -04:00
louispaulb
72e84d6b8d refactor(dispatch): la JOB impose sa compétence — retrait du niveau global par compétence
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>
2026-07-02 09:38:54 -04:00
louispaulb
aa6cc3e732 feat(dispatch): niveau requis persistant + lasso freeform + routes réelles Mapbox
- 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>
2026-07-02 09:18:33 -04:00
louispaulb
83338ca57b feat(acte): barème sous-traitant — transport/jour + FTTH 250m+ (base + $/m)
- Ajout acte 'transport' (Tarif transport, 100$/jour, unité qte) ; retrait de
  l'ancien 'tarif_horaire + transport' combiné (remplacé par transport + perte_temps).
- amountOf : palier « FTTH 250 m + » = base 150-250 m (120$) + 1,20$/m au-delà de 250 m
  (l'input mètres = distance TOTALE).
- Prix du barème global saisis via /acte/rates (35/26/26/14/29/19/21/60/80/120/…).

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00
louispaulb
6628eaaa5b feat(ops+hub): sender picker, per-user notifs, editable rating emails, planif & tickets UX
- 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>
2026-06-16 22:41:51 -04:00
louispaulb
1b7bad1686 security: harden payments/hub auth + remove leaked ERPNext token from source
Auth hardening:
- payments: per-customer JWT authorization on dual-use /payments routes
  (balance/methods/invoice/checkout/setup/portal/toggle-ppa) via authorizeCustomer
  + PAYMENTS_AUTH=enforce; portal retains+sends magic-link JWT (sessionStorage)
- hub: fail-closed Stripe webhook, /accept/doc-pdf IDOR gate, telephony field-name
  guard (SQLi), modem-bridge private-IP guard (SSRF), ALWAYS_ENFORCE expansion

Leaked-credential cleanup (token already rotated in ERPNext):
- de-hardcode ERPNext API token -> env in bulk_submit.py, import_items.py,
  apps/client/deploy.sh; placeholder in apps/ops/infra/nginx.conf (nginx injects)
- ops prod build no longer bakes VITE_ERP_TOKEN (.env.production empty)
- de-hardcode legacy DB password -> env in import_items.py
- gitignore legacy migration PII exports (tsv/json)

Conversations/UI:
- floating (un-docked) conversation panel; full-width mailbox
- per-message real sender from email headers; unified scroll; header spacing
- campaign pre-send review (subject/from/channel), Gmail send channel, clone-as-draft

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 06:17:17 -04:00
louispaulb
54f0d271dd feat(inbox): global thread scroll + close (X) on another agent's draft mirror
- 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>
2026-06-15 08:09:07 -04:00
louispaulb
f519bb3dd6 feat(inbox): email height fits content + per-message reply button
- 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>
2026-06-15 07:29:34 -04:00
louispaulb
08069ec0dc feat(inbox): show the agent's real name as sender + cleaner Gmail-style thread
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>
2026-06-15 07:15:23 -04:00
louispaulb
9466645e17 fix(inbox): never resolve our own domains to a customer (wrong sender names)
Root cause: some F customer records carry our own/staff addresses in email_billing,
so matchCustomer's `email_billing LIKE '%addr%'` resolved support@targo.ca -> "Guylaine
Gagnon" and gilles@targointernet.com -> "Sylvie Juteau". Every thread on one of our
addresses then inherited that wrong customer + name.

- OWN_DOMAINS promoted to a single source in lib/helpers.js (was duplicated in
  conversation.js); inbox-triage.matchCustomer() now returns null for any own-domain
  address — a customer is never reachable at targo.ca / targointernet.com / gigafibre.ca.
- conversation.js consumes the shared OWN_DOMAINS (removes the local copy).

Also ran a one-shot repair (temporary endpoint, since removed) that unlinked the 6
already-contaminated threads and reset their display name to the real address.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:12:12 -04:00
louispaulb
19d31025a6 feat(inbox+historique): sender-identity fix, single-source taxonomy, dispatch history & leaderboards
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>
2026-06-15 06:11:47 -04:00
louispaulb
aa108ab13f feat(field-tech): app Capacitor native (geofence Transistorsoft + scan MLKit) + CI
App technicien : appairage 1x (QR), géorepérage natif en arrière-plan (app fermée)
-> checkpoints au hub /field/ts, scan série/MAC on-device MLKit. UI réutilisée depuis
le hub (/field). APK Android buildé (debug, 19 Mo, arm64-v8a + armeabi-v7a).

- apps/field-tech : Capacitor 6 + Vite ; src/main.js (appairage @capacitor/preferences
  + BackgroundGeolocation.addGeofences + redirection /field) ; projet android/ avec les
  4 correctifs Gradle commités (force work-runtime 2.9.1, minSdk 24, repos maven xms.g
  + Huawei, googlePlayServicesLocationVersion 21.0.1) ; abiFilters arm (APK 32->19 Mo).
- README : recette de build complète (toolchain M4, les 4 fixes, install/appairage) ;
  BUILD-ANDROID.md (Docker + Android Studio + release signée) ; CI-SETUP.md (runner Gitea).
- CI Gitea Actions (.gitea/workflows) : android (runner Linux HORS prod, cache Gradle/npm
  -> artefact APK) + ios (runner macOS, workflow_dispatch, en attente compte Apple).

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:32:00 -04:00
louispaulb
4a3dc01938 Planification grille : retrait de l'axe de temps + la bande n'intercepte plus le clic/drag de la cellule
- 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>
2026-06-07 18:02:39 -04:00
louispaulb
29054c49c2 Planification éditeur journée : déplacements en pointillés + clic pin = détails + clic ligne = recentre carte
- 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>
2026-06-07 17:50:52 -04:00
louispaulb
34b10e0a1e Planification : carte de journée INTERACTIVE (Mapbox GL) — itinéraire routier réel + zoom/déplacement
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>
2026-06-07 17:44:22 -04:00
louispaulb
1673f7cd9a Pont : import basé sur l'ADRESSE DE SERVICE (delivery), pas la facturation (compte)
Problème de fond : quand ticket.delivery_id ne se joignait pas, le pont retombait sur l'adresse de
FACTURATION du compte (résidence) → sujet + géocodage faux. Or les 124 tickets ouverts-3301 ont tous un
delivery côté compte (98 via ticket.delivery_id, 26 via le compte).

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:30:11 -04:00
louispaulb
ab5eceb961 Conformité : repli « centre du code postal / ville » pour les unmatched restants (statut 'area')
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>
2026-06-07 17:14:41 -04:00
louispaulb
ed651d3ce9 Campings : gestion du registre + réapplication self-service depuis Ops
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>
2026-06-07 17:07:15 -04:00
louispaulb
e278d0d7b3 Campings : registre + géoloc de remplacement FIXE sur tous les lots (1405 corrigés)
Problème : pour un lot de camping, l'adresse du Service Location = souvent la RÉSIDENCE du client, et la
rue interne (ex. « 2 rue Canard, Lac des Pins ») n'est pas dans le RQA → géoloc fausse. Solution data-driven :
table `camping_registry` (mot-clé ville → nom + adresse principale + GPS fixe), coords relevées dans le
legacy delivery. Application : force lat/long du camping sur tous les lots (match ville normalisée), garde
address_line (n° de terrain visible), linked_address = le camping, statut validated.

Appliqué : Lac des Pins 1242 · Dauphinais 134 · SandySun 28 · Frontière 1 (+ Ensoleillé en registre).
Idempotent + ré-applicable. scripts/migration/camping_registry.sql.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:04:03 -04:00
louispaulb
5d4f66108d Planification : minimap du territoire (pins+tracé) dans l'éditeur de journée + retrait du sélecteur priorité
- 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>
2026-06-07 16:54:53 -04:00
louispaulb
fbf584f02f Phase 1 (hygiène) : utils partagés + logique pure testable + observabilité erp + 1ers tests
Modularisation / dé-duplication :
- lib/util/text.js : `norm` canonique partagé (remplace 2 ré-implémentations : address-db, legacy-dispatch-sync).
- lib/util/legacy-parse.js : parseurs/mapping PURS du pont (DEPT_JOBTYPE, DUR, jobType, prio, tzDate,
  startTime, coord) extraits hors I/O → testables en isolation, sans pg/mysql/erp.
- legacy-dispatch-sync + address-db importent ces utils (pont vérifié en prod : preview OK, 0 erreur).

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:36:41 -04:00
louispaulb
fc95a6cd4f Optimisation (consolidation helpers address) + doc Vision/modularisation
Optimisation sûre (vérifiée, 0 régression) :
- helpers.js : `cors()` partagé (en-têtes CORS génériques) au lieu de 2 copies locales.
- address-conformity.js : réutilise `pool` (address-db) + `cors` (helpers) au lieu de redéfinir un Pool +
  cors → 1 seul client pg local partagé pour rqa_addresses/fiber.
- address-validate.js : utilise helpers.cors.

docs/architecture/VISION.md (NOUVEAU) — vision + plan de modularisation + roadmap d'optimisation, fondé sur
un audit chiffré (hub 58 modules/23k lignes, Ops 45k lignes, god-files identifiés). Découpe en 9 domaines
(bounded contexts), principe « source de vérité + validation à la saisie + lien stable » (modèle Adresses
généralisé à Client/Device/Service), optimisations P0/P1/P2, métriques de succès. Complète les docs
architecture existants + ENGINEERING_PRACTICES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:28:54 -04:00
louispaulb
72d23665b2 Page Ops « Conformité des adresses » — source de vérité unique pour résoudre le backlog
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>
2026-06-07 01:06:55 -04:00
louispaulb
d17da8c40c SL normalisation passe 2 : récupération GPS par numéro+ville (sans contrainte postale)
scripts/migration/normalize_service_locations_pass2.sql : pour les SL restées review/unmatched après la
passe 1 (code postal+numéro), rematch par NUMÉRO + VILLE (trigram, normalisation St→Saint) + meilleure rue
par similarité ≥0.30 (rue seule, pas ville-incluse pour éviter le gonflement) → upgrade en validated avec
coords RQA réelles. Récupère les SL au code postal erroné/manquant mais ville valide (ex. Athelstan/Ch Ridge).

Résultat : +264 validated (15 195), unmatched 766→550. GPS sur 17 036/17 111 services (99,6%),
dont 15 195 (89%) rooftop AQ. Les 75 sans GPS = boîtes postales/hors-QC/placeholders/sobriquets camping
(pas de position de service réelle). Idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 16:19:37 -04:00
louispaulb
ef6a993e6b Normalisation AQ des 17k Service Locations (lien RQA + statut, adresse originale préservée)
scripts/migration/normalize_service_locations.sql : rattache chaque Service Location à l'adresse
canonique Adresses Québec (table locale rqa_addresses) par CODE POSTAL + NUMÉRO (clé sélective indexée)
+ meilleure rue par similarité désaccentuée (unaccent + pg_trgm). Les 2 tables étant colocalisées dans
la db ERPNext, tout se fait en SQL (~26 s pour 16 345 lignes, pas d'aller-retour applicatif).

Remplit (sans toucher address_line/city/postal_code = origine préservée → table de translation) :
- aq_address_id = rqa_addresses.id (clé de translation locale ; ⚠ id LOCAL, pas l'uuid AQ officiel absent
  de la table locale)
- linked_address = adresse canonique conforme (address_full)
- address_validation_status = validated (sim≥0.20) | review (sim<0.20, ex. surnoms de camping) | unmatched
- latitude/longitude raffinées seulement si validated

Résultat : validated 14 931 (87%) · review 1 414 (8%) · unmatched 766 (4,5%) → 16 345 liés (95,5%).
Idempotent (ne traite que les lignes 'pending'). Couvre la tâche « backfill normalisation 17k SL ».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:59:21 -04:00
louispaulb
ccad864165 Recherche d'adresses : base LOCALE (Postgres ERPNext) au lieu du Supabase cloud externe
Le hub n'appelle plus rddrjzptzhypltuzmere.supabase.co. La base RQA + dispo fibre est DÉJÀ locale
dans le Postgres ERPNext (rqa_addresses 5,24M + fiber_availability 21,6k jointes par address_id),
le hub y accède (réseau erpnext_erpnext + module pg).

- NOUVEAU lib/address-db.js : recherche locale. Phase 1 (civique présent) = filtre numero btree +
  mots de rue → ~20-150 ms ; Phase 2 (sans civique) = word_similarity (`<%` indexable GIN) au lieu de
  similarity() plein (24-76 s sur 5,24M !) → ~700 ms, dans une txn SET LOCAL (seuil 0.6 + statement_timeout 4s).
  Renvoie 2 formes : searchLocal (mappée, compat historique) + searchRaw (colonnes brutes de la fonction).
- address-search.js : searchAddresses + searchAddressesRpc délèguent à address-db (plus aucun appel Supabase).
  → onboarding (/address/validate), checkout (/api/address-search) ET le pont (géocodage) passent en LOCAL.
- address-validate.js : endpoints PUBLICS pour le site web (CORS) — POST /rpc/search_addresses (compat
  Supabase RPC, tableau direct) + GET /address/search — servis depuis le PG local (fiber_available inclus).
- server.js : route /rpc/ → address-validate.

Résultat pont (vérifié) : couverture 112/125 (vs 109 via Supabase), rqa_geocode 8→25 (table locale plus
complète + search_text désaccentué), Mapbox 37→23, no_coords 16→13, 0 erreur. Le local est meilleur.
Env hub : ADDR_DB_* dans /opt/targo-hub/.env (défauts erpnext-db-1/_eb65bdc0c4b1b2d6).

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:05:14 -04:00
louispaulb
e14026ae23 Pont legacy : coords GPS fiables (delivery→SL→RQA→Mapbox) + routage routier réel (Mapbox Matrix)
Pont (legacy-dispatch-sync.js) :
- Import des coordonnées par job via cascade : table legacy `delivery` (point de service exact,
  JOIN ticket.delivery_id) > Service Location ERPNext > géocodage RQA > géocodage Mapbox.
  Validation bornes Québec (coord()). Couverture 153/172 (89%).
- Géocodage RQA corrigé : retrait du générique de voie (Rue/Rang/Chemin absent de
  odonyme_recompose_normal) + code postal non accolé au terme (sinon ilike ne matche jamais).
- Repli Mapbox geocoding pour rues trop récentes pour le RQA (MAPBOX_TOKEN).
- Backfill + UPGRADE : coords delivery écrasent des coords SL moins précises (jamais l'inverse).
- Comptabilité honnête : vérifie r.ok sur create/update (erp ne throw pas) → errors + error_samples.
- Verrou de sérialisation sync() : tick + runs manuels ne se chevauchent plus (frappe_pg).
- Subject tronqué à 140 (champ Data) → corrige CharacterLengthExceededError sur jobs sans SL.
- Observabilité : coord_src tally + error_samples dans le résumé.

Ops Planification (éditeur de journée) :
- travelBetween() consulte une matrice Mapbox Matrix chargée à l'ouverture (loadDayRoute) →
  temps de trajet ROUTIERS RÉELS ; réordonnancement instantané sans nouvelle requête.
  Repli haversine si Mapbox indispo. Indicateur 🚗 réel vs 📏 vol d'oiseau.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:43:34 -04:00
louispaulb
22fbb81d3e feat(planif): éditeur journée = planificateur de tournée (heures recalculées, pas d'overlap, RDV verrouillables, détails)
- FIX overlap/ordre : packedDay recalcule les heures depuis la SÉQUENCE (ordre liste) + durées + transport ;
  le timeline et les heures affichées en découlent → réordonner/allonger repousse les suivants, plus d'overlap
- RDV à heure FIXE : verrou par job (🔒, défaut = booking_status 'Confirmé') → garde son heure ; flexibles = enchaînés
- clic sur un job → détails (legacy_detail : dates + description) pour juger urgence/durée ; + tooltip au survol
- save persiste start_time recalculé (+ route_order/priority/duration_h) via reorder-jobs
- hub occupancy renvoie booking_status/legacy_detail/legacy_id ; reorder-jobs accepte start_time
- fix collision fmtH → fmtHM (HH:MM padded)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:56:26 -04:00
louispaulb
faea3b0c1a feat(planif): éditeur journée — réordonnancement fiable (flèches+drop) + durée éditable (min) + temps de transport
- 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>
2026-06-06 13:41:28 -04:00