6.6 KiB
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), notdayjs(t).utc(). The original deliveries page had a UTC bucketing bug that hid orders mid-day; the multi-line comment abovegetRowBatchIdindeliveries.js(search forgetRowBatchId) explains it. Don't reintroduce. - Don't bucket bare
YYYY-MM-DDstrings — 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
useRefmap holds Leaflet marker instances keyed byorderid. Opening/closing popups is done by callingmarker.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 (viauseMemo) from the loaded order list filtered by the active batch. Mirror this shape ondeliveries.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 oruseMemo. - Don't put
setSelectedRiderinside auseEffectthat depends onriders— that causes polling-loop re-renders. - Don't
console.loginside 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-dndwithreact-dnd-html5-backend. Don't swap libraries. - After every drop, debounce a call to
reconcileStepsfromapi.js. Don't call it synchronously on every drag tick — the optimiser will rate-limit you. - The "Assign" button calls
finalCreatedeliveries→ triggersnotifyRiderfor 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 throughorders.js(the mode selector), and add the corresponding endpoint toapi.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.