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>
333 lines
23 KiB
Markdown
333 lines
23 KiB
Markdown
# 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](../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](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](../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 `+ WO` → `WoCreateModal.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](../architecture/overview.md) #6.4: only one `deviceId` per request — poll in parallel.
|
||
|
||
### Twilio
|
||
|
||
- **Outbound** — `PublishScheduleModal` calls `sendTestSms` to deliver the magic link and webcal URL per tech.
|
||
- **Inbound** — `tech-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](../architecture/overview.md) §1.
|
||
|
||
## 7. Cross-module References
|
||
|
||
- [../architecture/overview.md](../architecture/overview.md) — ecosystem map, retirement plan for the legacy dispatch-app and field app.
|
||
- [../architecture/data-model.md](../architecture/data-model.md) — canonical Dispatch Job / Dispatch Technician field list.
|
||
- [flow-editor.md](flow-editor.md) — wires `on_open_webhook` / `on_close_webhook` on Dispatch Job into workflow runs.
|
||
- [vision-ocr.md](vision-ocr.md) — barcode / equipment recognition consumed by the tech mobile page during a dispatch.
|
||
- [cpe-management.md](cpe-management.md) — "Diagnostiquer" deep-dives started from a customer card flow into the same tech assignment surface.
|
||
- [billing-payments.md](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](../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.js` → `lookupTechByPhone` |
|
||
| 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](../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](../README.md) · [roadmap.md](../roadmap.md)
|