# 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
// BATCH_OPTIONS — 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 `'assigned'` → bucket key is `['assigntime']`. Other options use other timestamp fields (`pickedtime`, `deliverytime`). If you add a new time field option, make sure `deliveries.js` is updated too — they read each other's bucketing.

### 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 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.
