Files
Doormilexpress_console/src/pages/nearle/dispatch/CLAUDE.md

6.6 KiB
Raw Blame History

CLAUDE.md — src/pages/nearle/dispatch/

Rules for editing Dispatch.js, Preview.js, CompareDataPanel.js, and dispatchShared.js. This is the most complex area of the codebase. Dispatch.js alone is ~2500 lines, mixes Leaflet imperative APIs with React state, and shares its batch / time-bucket model with deliveries.js. Read this before touching anything here.


1. Batch / wave model

Dispatch.js defines the canonical batch hour ranges. deliveries.js mirrors them — the two pages must agree on which batch a given row belongs to, otherwise the same delivery shows up in one batch on one page and a different batch on the other.

// BATCHES_DEFAULT_RAW — half-open [startHour, endHour) in LOCAL time, not UTC
[
  { id: 'morning',   startHour: 0,  endHour: 8  },   // 12 AM – 8 AM
  { id: 'afternoon', startHour: 9,  endHour: 12.5 }, // 9 AM – 12:30 PM
  { id: 'evening',   startHour: 16, endHour: 19 }    // 4 PM – 7 PM
]

Gaps are intentional (8–9 AM, 12 PM–4 PM, after 7 PM). Rows that fall in a gap belong to no batch — not to the nearest one.

Time-field selection (selectedTimeField)

Default 'due' → bucket key is ['expecteddeliverytime'] (the booking's promised delivery slot, serviceoptions[0].estimateddeliveryat). deliveries.js hardcodes the same key in BATCH_TIME_KEYS. If you change one, change both — they read each other's bucketing.

⛔ Never bucket on assigntime

It is not an assignment time. The Doormile bookings feed has no assignment timestamp, so api.js maps assigntime to the booking's updatedat — its last-modified column. Any status change, parcel scan, payment or pickup-complete re-stamps it.

Bucketing on it (which both pages did until this was found) means an order silently leaves the batch it belongs to and joins whichever window contains the current clock time, so the same orders appear under Afternoon and then Evening on the same day. The Dispatch page's 15-second poll makes the counts move on their own.

Displaying it as a "last updated" stamp is fine — that's all the reports use it for. The true assignment record lives on bookingassignments, reachable only per-booking via GET /admin/bookings/:id/track ({ booking, assignments[], riders[] }); its assignments[] has not been observed non-empty, so it is not a verified source yet. If the backend ever exposes an assignedat on the list feed, that becomes the correct bucket key.

Don'ts

  • Don't bucket in UTC. Use dayjs(t) (local), not dayjs(t).utc(). The original deliveries page had a UTC bucketing bug that hid orders mid-day; the multi-line comment above getRowBatchId in deliveries.js (search for getRowBatchId) explains it. Don't reintroduce.
  • Don't bucket bare YYYY-MM-DD strings — they parse to midnight and mis-bucket into Morning. Skip them.
  • Don't bucket on a timestamp that the backend re-stamps. See above.
  • Don't add a 4th batch without updating both pages and confirming with the backend what the new boundary means for assignments.

2. Leaflet integration

Dispatch.js uses react-leaflet for declarative tile/marker rendering, BUT a lot of marker behaviour is imperative:

  • Marker icons: The default leaflet marker icons are loaded from a CDN at module top (line ~454) via L.Icon.Default.mergeOptions. Webpack can't bundle the default sprite, so don't remove this shim.
  • Polyline offset: import '../../../utils/leafletPolylineOffset' adds a polyline-offset method to leaflet. Required for showing two riders on the same road segment with parallel polylines. Don't remove the import; the symbol it adds is used at the prototype level.
  • Imperative marker registry: A useRef map holds Leaflet marker instances keyed by orderid. Opening/closing popups is done by calling marker.openPopup() / closePopup() directly — not by setting React state. This is intentional; flipping state caused full-list re-renders and dropped frames.
  • Popup overlay: The "centered" popup the operator sees on hover is rendered outside Leaflet (it's a plain MUI dialog absolutely-positioned over the map). Leaflet's <Popup> attached to the marker is for click context only. Don't try to consolidate them.

3. The reconcile rule (re-stated because it's load-bearing)

After any manual edit on /doormile/dispatch/preview (drag-and-drop step reorder, swap rider, change delivery sequence), the page must call POST routes.workolik.com/optimization/reconcile-steps before POST jupiter.nearle.app/deliveries/createdeliveries.

Skipping reconcile corrupts route sequences in the database. This is the single biggest production bug to avoid in this area.


4. State that drives the live map

  • riders — array of rider objects with latest GPS. Updated by polling /utils/getriderperiodiclogs.
  • selectedRider — currently focused rider. Triggers polyline highlight + popup reveal.
  • batchCounts — derived (via useMemo) from the loaded order list filtered by the active batch. Mirror this shape on deliveries.js.
  • selectedTimeField — which timestamp drives bucketing. See §1.

5. Performance constraints

The dispatch page renders 100+ markers and polylines on every render. Watch for these regressions:

  • Don't pass new object literals to memoised child components ({ size: 12 }-style props re-trigger re-render). Lift to refs or useMemo.
  • Don't put setSelectedRider inside a useEffect that depends on riders — that causes polling-loop re-renders.
  • Don't console.log inside a marker click handler in production paths.

6. Editing Preview.js specifically

Preview.js is the post-solver staging page. Receives the solver output, lets the operator drag-and-drop, then commits.

  • Drag-and-drop uses react-dnd with react-dnd-html5-backend. Don't swap libraries.
  • After every drop, debounce a call to reconcileSteps from api.js. Don't call it synchronously on every drag tick — the optimiser will rate-limit you.
  • The "Assign" button calls finalCreatedeliveries → triggers notifyRider for each rider in the payload → redirects to /doormile/deliveries. Don't reorder these three steps.

7. File expectations for new work

  • New solver mode? Add a new constant in dispatchShared.js, wire it through orders.js (the mode selector), and add the corresponding endpoint to api.js. Don't hardcode a new URL inside Dispatch.js or Preview.js.
  • New rider attribute on the live map? Add it to the rider-popup component in Dispatch.js, not to a new component — Leaflet popup mounting is fragile across React tree changes.
  • Don't break CompareDataPanel.js — it's used by the QA team to A/B different solver runs.