# 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. ```js // 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 `` 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.