gigafibre-fsm/docs/features/dispatch.md
louispaulb 30a867a326 fix(tech): restore Gemini-native scanner + port equipment UX into ops
The ops tech module at /ops/#/j/* had drifted from the field app in two ways:

1. Scanner — a prior "restoration" re-added html5-qrcode, but the
   design has always been native <input capture="environment"> → Gemini
   2.5 Flash via targo-hub /vision/barcodes (up to 3 codes) and
   /vision/equipment (structured labels, up to 5). Revert useScanner.js
   + ScanPage.vue + TechScanPage.vue to commit e50ea88 and drop
   html5-qrcode from both package.json + lockfiles. No JS barcode
   library, no camera stream, no polyfills.

2. Equipment UX — TechJobDetailPage.vue was a 186-line stub missing the
   Ajouter bottom-sheet (Scanner / Rechercher / Créer), the debounced
   SN-then-MAC search, the 5-field create dialog, Type + Priority
   selects on the info card, and the location-detail contact expansion.
   Port the full UX from apps/field/src/pages/JobDetailPage.vue (526
   lines) into the ops module (458 lines after consolidation).

Rebuilt and deployed both apps. Remote smoke test confirms 0 bundles
reference html5-qrcode and the new TechJobDetailPage.1075b3b8.js chunk
(16.7 KB vs ~5 KB stub) ships the equipment bottom-sheet strings.

Docs:

- docs/features/tech-mobile.md — new. Documents all three delivery
  surfaces (legacy SSR /t/{jwt}, transitional apps/field/, unified
  /ops/#/j/*), Gemini-native scanner pipeline, equipment UX, magic-link
  JWT, cutover plan. Replaces an earlier stub that incorrectly
  referenced html5-qrcode.
- docs/features/dispatch.md — new. Dispatch board, scheduling, tags,
  travel-time optimization, magic-link SMS, SSE updates.
- docs/features/customer-portal.md — new. Plan A passwordless magic-link
  at portal.gigafibre.ca, Stripe self-service, file inventory.
- docs/architecture/module-interactions.md — new. One-page call graph
  with sequence diagrams for the hot paths.
- docs/README.md — expanded module index (§2) now lists every deployed
  surface with URL + primary doc + primary code locations (was missing
  dispatch, tickets, équipe, rapports, telephony, network, agent-flows,
  OCR, every customer-portal page). New cross-module edge map in §4.
- docs/features/README.md + docs/architecture/README.md — cross-link
  all new docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 15:56:38 -04:00

23 KiB
Raw Blame History

Dispatch

Real-time technician scheduling, work-order creation, and field publishing for the Ops PWA. Largest module in apps/ops/ — the single page where CSRs turn incoming service requests into scheduled work on a technician's calendar.

1. Goals & Principles

Dispatch is the live control surface for every field intervention. It replaces the legacy PHP dispatch-app (slated for retirement per ../architecture/overview.md) with a Vue 3 / Pinia timeline bound directly to ERPNext Dispatch Job and Dispatch Technician DocTypes.

Principles enforced in the code:

  • Single source of truth is ERPNext. Every drag, resize, tag toggle issues a PUT /api/resource/Dispatch Job/{name} through src/api/dispatch.js. The Pinia store (src/stores/dispatch.js) only holds a mapped view; on reload it rehydrates from Frappe.
  • Optimistic UI + undo stack. useUndo snapshots mutations so keyboard Cmd+Z can roll back a mistake before ERPNext ever replies.
  • Match skill, not cost. Per feedback_dispatch_tags.md convention, tags carry a level 1-5 and auto-dispatch picks the lowest adequate match — experts are preserved for jobs that require them.
  • Push, don't poll. Timeline stays in sync via SSE topics dispatch and network from targo-hub — tech-absence SMS, outage alerts, OLT up/down events all land as toasts without a refresh.
  • Human and material resources share one grid. A Dispatch Technician row is either a human (with a phone / user) or a material asset (Véhicule, Nacelle, OTDR, Fusionneuse…) — the same drag-drop logic applies.
  • Work is not visible to techs until it is published. The published flag on a Dispatch Job gates both the mobile page (/t/{token}) and the iCal feed.

2. File Inventory

Front-end (Ops PWA)

Path Responsibility
apps/ops/src/pages/DispatchPage.vue Top-level page, composes all composables, owns SSE connection, renders timeline / week / month switcher plus inline map panel.
apps/ops/src/modules/dispatch/components/TimelineRow.vue One row per resource — renders shift, absence, travel, assist, ghost and job segments; emits drag / drop / resize events.
apps/ops/src/modules/dispatch/components/WeekCalendar.vue 7-day grid view with absence chips, ghost recurrences and day-load bars; handles planning mode shifts and on-call bands.
apps/ops/src/modules/dispatch/components/MonthCalendar.vue Month overview with per-day tech-count badges; shows selected tech's availability when in planning mode.
apps/ops/src/modules/dispatch/components/BottomPanel.vue Unassigned jobs tray with lasso selection, batch assign and auto-distribute buttons.
apps/ops/src/modules/dispatch/components/RightPanel.vue Details side-panel for a selected job — edit, move, geofix, unassign, set end date, remove assistant, assign pending, update tags.
apps/ops/src/modules/dispatch/components/JobEditModal.vue Inline edit of title, address, note, duration, priority and tags.
apps/ops/src/modules/dispatch/components/WoCreateModal.vue "Nouveau work order" modal — runs rankTechs + refineWithDrivingTimes on the top 3 candidates.
apps/ops/src/modules/dispatch/components/CreateOfferModal.vue Creates a job offer in broadcast / targeted / pool mode with a pricing preset.
apps/ops/src/modules/dispatch/components/OfferPoolPanel.vue Live offer feed with status chips (open, pending, accepted, expired, cancelled).
apps/ops/src/modules/dispatch/components/PublishScheduleModal.vue "Publier & envoyer l'horaire par SMS" — publishes jobs, builds per-tech magic link + webcal iCal URL, sends via Twilio.
apps/ops/src/modules/dispatch/components/SbModal.vue Generic overlay modal with header / body / footer slots.
apps/ops/src/modules/dispatch/components/SbContextMenu.vue Positioned context-menu wrapper used for right-click menus on techs and jobs.
apps/ops/src/modules/dispatch/components/MapPanel.vue Standalone map panel component — present but not imported by DispatchPage.vue, which uses an inline Mapbox block instead.
apps/ops/src/modules/dispatch/components/SuggestSlotsDialog.vue Client for /dispatch/suggest-slots (hub endpoint). Present but not imported by DispatchPage.vue — kept for future "Find me a time" entry point.
apps/ops/src/stores/dispatch.js Pinia store — maps ERPNext rows to UI-friendly shape, rebuilds tech queues, triggers GPS polling.
apps/ops/src/config/dispatch.js Row height constant, resource icon map (Véhicule, Nacelle, Fusionneuse…), re-exports HUB_SSE_URL.
apps/ops/src/api/dispatch.js All Frappe REST calls — fetchTechnicians, fetchJobsFast, updateJob, publishJobs, createJob, createTech.
apps/ops/src/composables/useScheduler.js Drives period navigation, busy lanes, ghost materialization.
apps/ops/src/composables/useDragDrop.js Drag sources / drop targets for jobs, techs and assistants.
apps/ops/src/composables/useAutoDispatch.js Client-side candidate ranking mirroring the hub algorithm.
apps/ops/src/composables/useJobOffers.js Offer-pool state + PRICING_PRESETS.
apps/ops/src/composables/useAbsenceResize.js Drag-to-resize absence bands.
apps/ops/src/composables/useTechManagement.js Create / delete / reassign technicians.
apps/ops/src/composables/useTagManagement.js Tag editor bindings for techs and jobs.
apps/ops/src/composables/useContextMenus.js Tech and job right-click menus.
apps/ops/src/composables/useSelection.js Lasso selection in the bottom panel.
apps/ops/src/composables/useBottomPanel.js Collapsed / pinned state of the unassigned tray.
apps/ops/src/composables/usePeriodNavigation.js 3-period buffer and infinite horizontal scroll.
apps/ops/src/composables/useResourceFilter.js Human / material / group filtering.
apps/ops/src/composables/useMap.js Mapbox GL layer and marker lifecycle.
apps/ops/src/composables/useUndo.js Per-mutation snapshots for Cmd+Z.
apps/ops/src/composables/useAddressSearch.js Mapbox geocoder wrapper used by Wo and Job modals.
apps/ops/src/components/dispatch/NlpInput.vue Natural-language "type-to-dispatch" shortcut.

Back-end (targo-hub)

Path Responsibility
services/targo-hub/server.js Routes /dispatch/* to dispatch.js, /dispatch/ical-token/:id and /dispatch/calendar/:id.ics to ical.js, /t/* to tech-mobile.js, /traccar/* to traccar.js, /magic-link/* to magic-link.js.
services/targo-hub/lib/dispatch.js POST /dispatch/best-tech, POST /dispatch/suggest-slots, POST /dispatch/create-job. Implements rankTechs, getTechsWithLoad, enrichWithGps, suggestSlots.
services/targo-hub/lib/ical.js HMAC-SHA256 token signing, VCALENDAR builder with America/Toronto VTIMEZONE, status + priority mapping, RRULE passthrough.
services/targo-hub/lib/traccar.js Bearer-auth proxy to the Traccar GPS server with a 60s device cache.
services/targo-hub/lib/magic-link.js POST /magic-link/generate, POST /magic-link/verify, POST /magic-link/refresh — JWT tokens, default 72h TTL, job-level and all-jobs-for-tech variants.
services/targo-hub/lib/tech-mobile.js Server-rendered mobile page at /t/{token} with status update, scan, vision, equipment and catalog endpoints.
services/targo-hub/lib/tech-absence-sms.js Inbound Twilio SMS → Gemini Flash NLU (regex fallback) → setTechAbsence / clearTechAbsence → SSE tech-absence broadcast.
services/targo-hub/lib/sse.js Topic fan-out used by the dispatch and network channels.

ERPNext (Frappe custom DocTypes)

Path Responsibility
erpnext/setup_fsm_doctypes.py Creates / patches custom fields on Dispatch Job and Dispatch Technician.

3. Data Model

Dispatch Job

Core Frappe fields plus these custom fields (from erpnext/setup_fsm_doctypes.py):

Field Type Purpose
customer Link → Customer Billing-side link.
service_location Link → Service Location Physical address + geocoded coordinates.
job_type Select Installation / Réparation / Maintenance / Retrait / Dépannage / Autre.
source_issue Link → Issue Origin ticket, when the job came from Support.
depends_on Link → Dispatch Job Blocks scheduling until the dependency is done.
parent_job Link → Dispatch Job Multi-step workflow (with step_order).
on_open_webhook, on_close_webhook Data Flow-editor callbacks — see flow-editor.md.
equipment_items, materials_used, checklist, photos Table Child tables fed by the mobile tech page.
actual_start, actual_end, travel_time_min Datetime / Int Field-recorded metrics.
completion_notes, customer_signature Text / Attach End-of-job capture.
published Check Gates visibility on the mobile page and iCal feed.
is_recurring, recurrence_rule, recurrence_end, pause_periods, template_id Mixed RRULE-based repeating jobs (materialized on demand).
continuous Check Emergency jobs that may span weekends / off-days.
assistants (child table) Table tech_id, tech_name, duration_h, note, pinned.
tags (child table) Table tag, level, required.

Dispatch Technician

Field Type Purpose
resource_type Select human or material.
resource_category Select Véhicule / Outil / Salle / Équipement / Nacelle / Grue / Fusionneuse / OTDR.
weekly_schedule Text / JSON Parsed by parseWeeklySchedule.
extra_shifts JSON (hidden) Per-date overrides.
absence_reason, absence_from, absence_until, absence_start_time, absence_end_time Mixed Current absence band.
traccar_device_id Data Links to a Traccar GPS device.
tags (child table) Table tag, level.

Full field definitions live in ../architecture/data-model.md.

4. UI Surfaces

All French strings below are pulled directly from DispatchPage.vue.

Top bar

"Ressources" (resource filter), "Filtres" (tag filter), "Aujourd'hui" (jump-to-today), "Jour / Semaine / Mois" view toggle, "Planning" mode for availability editing, "Carte" for the inline map, "Publier" for the publish modal, "+ WO" for the work-order creator, "ERP" status pill.

Timeline view (TimelineRow.vue)

One row per resource. Each row renders stacked segments:

Segment type Source Notes
shift weeklySchedule + extraShifts Active working window; drop targets enabled.
absence absence_from..absence_until or weekly "off" days Resizable via useAbsenceResize.
travel Computed between consecutive jobs Mapbox Directions API; non-draggable.
assist assistants[] on another tech's job Pinned assists cannot be dragged away.
ghost Unmaterialized recurrence Click → materialize concrete Dispatch Job.
job Assigned, scheduled Dispatch Job Primary drag source.

Drag sources: tech avatar (to split shifts), job blocks (to move / reassign), pinned assist blocks (to nudge helper). Emits select-tech, ctx-tech, job-dragstart, timeline-drop, block-move, block-resize, absence-resize, ghost-click, ghost-materialize.

Week view (WeekCalendar.vue)

Seven-day grid per resource. Distinguishes isExplicitAbsent (a typed absence record) from isScheduleOff (non-working weekday). In planning mode it draws availability bands and on-call shift overlays on top.

Month view (MonthCalendar.vue)

Month grid with per-day badges showing how many techs are available. In planning mode a sidebar surfaces the selected tech's weekly schedule and extra shifts.

Bottom panel (BottomPanel.vue)

Unassigned jobs tray. Supports:

  • Lasso selection (click-drag to select multiple)
  • Batch assign to a tech
  • Auto-distribute across available techs using useAutoDispatch
  • Collapse / pin state via useBottomPanel

Right panel (RightPanel.vue)

Details + pending-request slide-in. Emits edit, move, geofix, unassign, set-end-date, remove-assistant, assign-pending, update-tags.

Inline map

DispatchPage renders its own Mapbox panel using useMap and MAPBOX_TOKEN from config/erpnext.js. MapPanel.vue exists in the module but is not currently imported — kept for a possible future extraction.

Offer pool (OfferPoolPanel.vue, CreateOfferModal.vue)

Three offer modes surface as icon toggles in CreateOfferModal:

Mode Behaviour
broadcast Blast to every matching tech; first accepter wins.
targeted Single named recipient.
pool A named candidate list; first to accept wins.

Pricing is composed from PRICING_PRESETS (in useJobOffers) as displacement$ + hourlyRate$/h × duration. Offer status chips: open, pending, accepted, expired, cancelled.

Context menu (SbContextMenu.vue)

Right-click on a tech row or job block opens a context menu wired through useContextMenus — quick reassign, cancel, open in ERP, copy magic link, copy iCal URL.

Drag-drop rules (enforced in useDragDrop)

  • Drop on an active shift / within absence band = allowed (toggle absence).
  • Drop on a non-working weekday = blocked unless continuous=1.
  • Drop on a tech whose tags don't meet tags.required levels = warning toast but still allowed (CSR override).
  • Escape cancels any in-flight drag.
  • Delete while a job is selected opens the unassign confirmation.
  • Cmd+Z walks back the useUndo snapshot stack.

5. Key Flows

5.1 Create a work order

  1. CSR clicks + WOWoCreateModal.vue opens.
  2. Address input auto-geocodes via useAddressSearch (Mapbox geocoder).
  3. On submit, WoCreateModal calls findBestTech() which locally runs the ranking, then refines the top 3 with live driving times.
  4. createJob (in src/api/dispatch.js) POSTs to ERPNext.
  5. The store prepends the new job; SSE fan-out notifies other open Ops tabs.

5.2 Suggest the best tech (server side)

POST /dispatch/best-tech on targo-hub (lib/dispatch.js):

  • rankTechs computes a score per candidate using SCORE_WEIGHTS — proximityMultiplier=4, proximityMax=100 km, loadMultiplier=30, overloadPenalty=500, gpsFreshnessBonus=20.
  • Proximity uses a Euclidean km approximation at Montreal latitude (fast enough for ranking).
  • getTechsWithLoad tallies today's scheduled hours; anything above the configured cap triggers the overload penalty.
  • enrichWithGps adds a freshness bonus when a recent Traccar fix exists.

5.3 Suggest time slots

POST /dispatch/suggest-slots (lib/dispatch.js) returns up to N best windows across all techs:

  • 7-day horizon, capped at 2 slots per tech to diversify suggestions.
  • Default shift 08:00-17:00 (overridable per tech).
  • 15-minute travel buffer inserted before/after existing jobs.
  • SuggestSlotsDialog.vue is the front-end client — present in the repo but not yet wired into DispatchPage.vue; the current path is tech selection via WoCreateModal or manual drag.

5.4 Offer pool

  1. CSR opens CreateOfferModal, picks mode + pricing preset.
  2. Offer is persisted through useJobOffers.
  3. OfferPoolPanel subscribes to offer events and updates chip status.
  4. Accepting techs fire a state transition that converts the offer into an assigned Dispatch Job.

5.5 Publish & SMS schedule

PublishScheduleModal.vue — title "Publier & envoyer l'horaire par SMS":

  1. Operator selects a tech + date range.
  2. publishJobs(jobNames) from src/api/dispatch.js PUTs published: 1 on each job in parallel.
  3. For each tech, the modal fetches:
    • A magic link via POST /magic-link/generate (targo-hub, default 72h TTL) → produces the /t/{token} URL that opens the mobile tech page.
    • An iCal token via GET /dispatch/ical-token/{techId} → builds a webcal:// URL pointing at /dispatch/calendar/{techId}.ics?token=....
  4. Twilio sends two SMS:
    • "Mes tâches: {link}" — the magic link.
    • "Ajouter à mon calendrier: {webcal url}" — the iCal subscription URL.
  5. sendTestSms is invoked with reference_doctype = Dispatch Technician so every send is logged against the tech in ERPNext.

5.6 Absence via SMS (inbound)

services/targo-hub/lib/tech-absence-sms.js:

  1. Twilio inbound webhook hits handleAbsenceSms.
  2. lookupTechByPhone matches the last 10 digits of the sender against Dispatch Technicians.
  3. Gemini 2.5 Flash (OpenAI-compatible endpoint) parses intent + dates; a regex fallback catches "absent / malade / sick / formation / vacances" with simple date ranges if the LLM is down.
  4. setTechAbsence or clearTechAbsence PUTs to Dispatch Technician and broadcasts SSE tech-absence on the dispatch topic.
  5. Ops timelines update in-place; the operator sees a toast. A French confirmation SMS is returned to the tech.

5.7 Recurring jobs (ghosts → materialized)

  • A template job carries is_recurring=1 and an RFC 5545 recurrence_rule (e.g. FREQ=WEEKLY;BYDAY=SA,SU).
  • WeekCalendar and TimelineRow render upcoming occurrences as ghost segments computed client-side.
  • Clicking a ghost fires ghost-materialize, creating a concrete Dispatch Job with template_id set — subsequent edits diverge from the template.
  • pause_periods carves out vacation windows without deleting the template.

6. External Integrations

Traccar (GPS)

services/targo-hub/lib/traccar.js proxies /traccar/devices, /traccar/positions and a generic GET fallback with Traccar bearer-token auth preferred over Basic. Device metadata is cached for 60 seconds. The store (useGpsTracking) polls positions and feeds live gpsCoords, gpsSpeed, gpsOnline into each tech row — these in turn power the freshness bonus in the server-side ranker. Traccar limitation noted in ../architecture/overview.md #6.4: only one deviceId per request — poll in parallel.

Twilio

  • OutboundPublishScheduleModal calls sendTestSms to deliver the magic link and webcal URL per tech.
  • Inboundtech-absence-sms.js owns Twilio's incoming SMS webhook and drives the absence flow above.
  • Number +14382313838 per reference_twilio.md.

iCal / webcal

services/targo-hub/lib/ical.js:

  • Token is HMAC-SHA256 over {techId} with ICAL_SECRET, truncated to 16 hex chars.
  • Fetches jobs from past 7 days to future 60 days for the tech.
  • Emits RFC 5545 with a full America/Toronto VTIMEZONE (DST rules included).
  • Status: open → TENTATIVE, cancelled / completed → CANCELLED.
  • Priority: urgent → 1, medium → 5, everything else → 9.
  • Honors recurrence_rule — passes the RRULE through untouched.

Subscription URL handed to techs is webcal://msg.gigafibre.ca/dispatch/calendar/{techId}.ics?token=... — Apple Calendar and Google Calendar both handle this.

Mobile tech page (/t/{token})

services/targo-hub/lib/tech-mobile.js is a lightweight server-rendered page — not the Ops PWA. It authenticates the tech with a JWT magic link (verifyJwt), lists their published jobs for the day, and offers POST endpoints:

Route Purpose
GET /t/{token} Day view for the tech.
POST /t/{token}/status Flip status (In Progress, Completed).
POST /t/{token}/scan Record a barcode scan.
POST /t/{token}/vision Send a photo to /vision/barcodes for OCR.
POST /t/{token}/equip Install a Service Equipment row.
POST /t/{token}/equip-remove Decommission equipment.
GET /t/{token}/catalog Fetch the equipment catalog.
GET /t/{token}/equip-list Fetch equipment already on site.

The Ops PWA never opens this page — it lives alongside Dispatch to replace the retiring apps/field app. See ../architecture/overview.md §1.

7. Cross-module References

  • ../architecture/overview.md — ecosystem map, retirement plan for the legacy dispatch-app and field app.
  • ../architecture/data-model.md — canonical Dispatch Job / Dispatch Technician field list.
  • flow-editor.md — wires on_open_webhook / on_close_webhook on Dispatch Job into workflow runs.
  • vision-ocr.md — barcode / equipment recognition consumed by the tech mobile page during a dispatch.
  • cpe-management.md — "Diagnostiquer" deep-dives started from a customer card flow into the same tech assignment surface.
  • billing-payments.md — Sales Orders that trigger installation Dispatch Jobs (order_source, sales_order fields on the job).

8. Failure Modes

Symptom Likely cause Where to look
Timeline loads but all rows are empty ERPNext token revoked or Administrator "Generate Keys" clicked src/api/auth.js + note in ../architecture/overview.md §6.3
Drops appear to work then revert on refresh ERPNext exc swallowed — check console for [API] PUT Dispatch Job/... failed apps/ops/src/api/dispatch.js
iCal subscription returns 401 Wrong token or ICAL_SECRET changed — ask operator to re-copy the webcal URL services/targo-hub/lib/ical.js
Tech never receives magic link SMS Twilio rejected the number or published=0 on the selected jobs PublishScheduleModal.vue + Twilio console
Absence SMS ignored Phone number not matched in Dispatch Technician (last-10-digits LIKE) services/targo-hub/lib/tech-absence-sms.jslookupTechByPhone
GPS dot stale / missing Traccar token expired, cache 60s, or no traccar_device_id on the tech services/targo-hub/lib/traccar.js
Suggestion always picks same tech Overload penalty not firing — check getTechsWithLoad result for load cap services/targo-hub/lib/dispatch.js
SSE toasts stop arriving ForwardAuth dropped the session; reload to re-auth with Authentik apps/ops/src/pages/DispatchPage.vue SSE setup
Ghost occurrences missing RRULE parse failed client-side — malformed recurrence_rule apps/ops/src/stores/dispatch.js _mapJob
Tech mobile page shows "not found" JWT expired (default 72h) — operator must re-publish to regenerate services/targo-hub/lib/magic-link.js

9. Retirement Note

The legacy dispatch-app (PHP) and apps/field (mobile technician SPA) are slated for retirement in April-May 2026, per ../architecture/overview.md §1. The replacement pairing is:

  • Ops PWA Dispatch module — this document — for all scheduling, ranking, publishing.
  • Lightweight tech mobile page at /t/{token} (see §6) for the field-side experience.

Do not add new features to dispatch-app or apps/field. Any feature request should land in apps/ops/src/modules/dispatch/ or the tech-mobile handler in services/targo-hub/lib/tech-mobile.js.


Back to docs/README.md · roadmap.md