- Batch
- {/* Status-wise (time-field) filter is hidden for now per spec —
+ {shouldFetchLive && viewMode !== 'rider-info' && (
+
+ Batch
+ {/* Status-wise (time-field) filter is hidden for now per spec —
bucketing is locked to `assigntime`. Restore this block to bring
back the Delivered/Pending/Assigned/... dropdown.
@@ -3059,7 +3060,7 @@ const Dispatch = ({
)}
*/}
- {/* Slot editor (Edit slots button + panel) is hidden for now per
+ {/* Slot editor (Edit slots button + panel) is hidden for now per
spec — the three batches (Morning / Afternoon / Evening) are
fixed. Restore this block to bring back the operator-editable
start/end hours, add-slot, and reset-to-defaults controls.
@@ -3179,312 +3180,312 @@ const Dispatch = ({
)}
*/}
- {/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls
+ {/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls
horizontally when it overflows. */}
-
- {/* `key` forces a remount when the rider changes so the
+
+ {/* `key` forces a remount when the rider changes so the
MapContainer re-centers on the new coords (leaflet's
center prop is only read on mount). */}
-
-
-
- {/* Permanent banner above the pin — Nominatim
+
+
+
+ {/* Permanent banner above the pin — Nominatim
reverse-geocode tells the operator which
suburb/area the rider is in. Falls back to
a "Locating…" hint while the request is in
flight so the pin never looks unlabeled. */}
-
- {riderInfoArea?.area || 'Locating area…'}
-
-
-
+ {d.logdate ? `Last seen ${d.logdate}` : `${lat.toFixed(6)}, ${lon.toFixed(6)}`}
- )}
-
- {d.logdate ? `Last seen ${d.logdate}` : `${lat.toFixed(6)}, ${lon.toFixed(6)}`}
-
-
-
-
+
+
+
+
-
- )}
-
- );
- })()}
- >
- )}
+ )}
+
+ );
+ })()}
+ >
+ )}
+
-
- ) : (
-
-
-
- {/* Sidebar header — replaces the top-bar meta line. Hidden when a specific
+ ) : (
+
+
+
+ {/* Sidebar header — replaces the top-bar meta line. Hidden when a specific
rider is focused, since the focused-rider view already shows that rider's
stats prominently (name + Orders/Distance/Profit tiles). */}
- {!focusedRider && (
-
-
-
-
- RIDER DISPATCH
-
-
-
- {activeStats.label}
-
-
-
-
-
-
-
-
{activeStats.orders}
-
{activeStats.orders === 1 ? 'Order' : 'Orders'}
+ {!focusedRider && (
+
+
+
+
+ RIDER DISPATCH
+
+
+ {activeStats.label}
+
-
-
-
-
{activeStats.riders}
-
{activeStats.riders === 1 ? 'Rider' : 'Riders'}
+
+
+
+
+
+
{activeStats.orders}
+
{activeStats.orders === 1 ? 'Order' : 'Orders'}
+
+
+
+
+
+
{activeStats.riders}
+
{activeStats.riders === 1 ? 'Rider' : 'Riders'}
+
-
- )}
+ )}
- {/* Stats strip hidden for now — restore by removing this comment wrapper.
+ {/* Stats strip hidden for now — restore by removing this comment wrapper.
- {(() => {
- // Stack the status pill and delivery time vertically on
- // the right of the header so the operator sees the order
- // outcome and the wall-clock time it landed at a glance.
- const actual = formatTimeOnly(o.deliverytime);
- const expected = formatTimeOnly(o.expecteddeliverytime);
- const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
- const showEstDrop = !isDelivered && estMeters !== null;
- if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
- return (
-
- {/* Render the kitchen's orders with the same zone-order-card layout
- used by the focused-zone view, so By Location, By Zone, and By
- Rider all look consistent. Kitchen name is omitted from each
- card because the focused kitchen header already provides it. */}
-
+ {(() => {
+ // Stack the status pill and delivery time vertically on
+ // the right of the header so the operator sees the order
+ // outcome and the wall-clock time it landed at a glance.
+ const actual = formatTimeOnly(o.deliverytime);
+ const expected = formatTimeOnly(o.expecteddeliverytime);
+ const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
+ const showEstDrop = !isDelivered && estMeters !== null;
+ if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
+ return (
+
+ {/* Render the kitchen's orders with the same zone-order-card layout
+ used by the focused-zone view, so By Location, By Zone, and By
+ Rider all look consistent. Kitchen name is omitted from each
+ card because the focused kitchen header already provides it. */}
+
+
+
+ ))
+ }
+ {renderMarkers()}
+ {renderRoutes()}
+
+ {/* Live rider GPS markers from /partners/getriderlogs/. Mirrors the
Reports → Riders Logs map: green pin when the rider's last log
row is `active`, red otherwise, with the rider's username as a
label. Scoped to riders who actually have orders in the
@@ -4188,484 +4189,484 @@ const Dispatch = ({
rider with zero orders in the current slot is hidden, even if
getriderlogs still returns their GPS row. When a specific
rider is focused, only that one is shown. */}
- {liveRiderLocations
- .filter((r) => riders.some((rd) => String(rd.id) === String(r.id)))
- .filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))
- .map((r) => {
- const isActive = r.status === 'active';
- const pinColor = isActive ? '#16a34a' : '#dc2626';
- // Look up the rider's in-progress order so the popup can show
- // where they're heading next (drop customer/area + originating
- // kitchen). Falls back to nothing when every order is final.
- const matchingRider = riders.find((rd) => String(rd.id) === String(r.id));
- const nextOrder = matchingRider?.orders
- ?.slice()
- .sort((a, b) => {
- const tA = a.trip_number || 1;
- const tB = b.trip_number || 1;
- if (tA !== tB) return tA - tB;
- return (a.step || 0) - (b.step || 0);
- })
- .find((o) => {
- const s = String(o.orderstatus || '').toLowerCase();
- return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
- });
- const nextDropArea = nextOrder
- ? (nextOrder.deliverysuburb || extractArea(nextOrder.deliveryaddress))
- : null;
- const liveIcon = L.divIcon({
- className: '',
- iconSize: [140, 56],
- iconAnchor: [12, 41],
- popupAnchor: [58, -40],
- html: `
+ {liveRiderLocations
+ .filter((r) => riders.some((rd) => String(rd.id) === String(r.id)))
+ .filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))
+ .map((r) => {
+ const isActive = r.status === 'active';
+ const pinColor = isActive ? '#16a34a' : '#dc2626';
+ // Look up the rider's in-progress order so the popup can show
+ // where they're heading next (drop customer/area + originating
+ // kitchen). Falls back to nothing when every order is final.
+ const matchingRider = riders.find((rd) => String(rd.id) === String(r.id));
+ const nextOrder = matchingRider?.orders
+ ?.slice()
+ .sort((a, b) => {
+ const tA = a.trip_number || 1;
+ const tB = b.trip_number || 1;
+ if (tA !== tB) return tA - tB;
+ return (a.step || 0) - (b.step || 0);
+ })
+ .find((o) => {
+ const s = String(o.orderstatus || '').toLowerCase();
+ return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
+ });
+ const nextDropArea = nextOrder
+ ? (nextOrder.deliverysuburb || extractArea(nextOrder.deliveryaddress))
+ : null;
+ const liveIcon = L.divIcon({
+ className: '',
+ iconSize: [140, 56],
+ iconAnchor: [12, 41],
+ popupAnchor: [58, -40],
+ html: `
- Last Seen
-
- {' '}
- {dayjs(r.logdate).isValid() ? dayjs(r.logdate).format('hh:mm:ss A') : r.logdate}
-
-
- )}
-
- Position
-
- {r.lat.toFixed(5)}, {r.lon.toFixed(5)}
-
-
-
-
-
- );
- })}
+
+
+ );
+ })}
- {/* Compare mode — actual GPS tracks for the focused rider, drawn
+ {/* Compare mode — actual GPS tracks for the focused rider, drawn
on the same MapContainer as the planned route. Gated by
compareViewMode so the segmented control on the map's top-left
can hide them ("Planned only") or hide the planned polylines
instead ("Actual only"). In Combined mode both render and the
planned polylines switch to a dashed stroke (see renderRoutes)
so the operator can read overlap at a glance. */}
- {compareOpen && focusedRider && compareViewMode !== 'planned' && (riderActualTracks.map((t, i) => {
- if (t.coords.length === 0) return null;
- // `color` drives the drop pin, start pin, and tooltip header so
- // those keep their per-step palette identity (the same colors
- // the timeline uses). `polylineColor` is what the GPS line
- // itself draws with — collapsed to a single emerald in Combined
- // view so the actual layer reads as one cohesive trail next to
- // the indigo planned rail; Actual-only mode keeps step palette
- // on the polyline since there's no second layer to confuse with.
- const color = stepColor(i);
- const polylineColor = compareViewMode === 'combined'
- ? COMBINED_ACTUAL_COLOR
- : color;
- const startPos = [t.coords[0].lat, t.coords[0].lng];
- const endPos = [t.coords[t.coords.length - 1].lat, t.coords[t.coords.length - 1].lng];
- const snapped = osrmTrackRoutes[t.deliveryid];
- const hasRoad = Array.isArray(snapped) && snapped.length >= 2;
- const fullPositions = hasRoad
- ? snapped
- : t.coords.map((p) => [p.lat, p.lng]);
+ {compareOpen && focusedRider && compareViewMode !== 'planned' && (riderActualTracks.map((t, i) => {
+ if (t.coords.length === 0) return null;
+ // `color` drives the drop pin, start pin, and tooltip header so
+ // those keep their per-step palette identity (the same colors
+ // the timeline uses). `polylineColor` is what the GPS line
+ // itself draws with — collapsed to a single emerald in Combined
+ // view so the actual layer reads as one cohesive trail next to
+ // the indigo planned rail; Actual-only mode keeps step palette
+ // on the polyline since there's no second layer to confuse with.
+ const color = stepColor(i);
+ const polylineColor = compareViewMode === 'combined'
+ ? COMBINED_ACTUAL_COLOR
+ : color;
+ const startPos = [t.coords[0].lat, t.coords[0].lng];
+ const endPos = [t.coords[t.coords.length - 1].lat, t.coords[t.coords.length - 1].lng];
+ const snapped = osrmTrackRoutes[t.deliveryid];
+ const hasRoad = Array.isArray(snapped) && snapped.length >= 2;
+ const fullPositions = hasRoad
+ ? snapped
+ : t.coords.map((p) => [p.lat, p.lng]);
- let positions = fullPositions;
- let drawPolyline = true;
- if (isAnimating) {
- const progress = animatedActualProgress[t.sequenceStep] || 0;
- if (progress < 2) {
- drawPolyline = false;
- } else {
- positions = fullPositions.slice(0, Math.min(progress, fullPositions.length));
+ let positions = fullPositions;
+ let drawPolyline = true;
+ if (isAnimating) {
+ const progress = animatedActualProgress[t.sequenceStep] || 0;
+ if (progress < 2) {
+ drawPolyline = false;
+ } else {
+ positions = fullPositions.slice(0, Math.min(progress, fullPositions.length));
+ }
}
- }
- const isFocusedStep = focusedCompareStep === t.sequenceStep;
- const statusLow = String(t.orderstatus || '').toLowerCase();
- const isDelivered = FINAL_STATUSES.has(statusLow);
- const isSkipped = SKIPPED_STATUSES.has(statusLow);
- const delta = compareDeltas.find((d) => d.sequenceStep === t.sequenceStep);
- const isAnomaly = !!delta?.anomaly;
- // Look up the corresponding order in the focused rider's set so
- // the actual-track drop pin can render the same rich popup the
- // planned-route number marker uses (Timeline, Details, KM chips).
- // Matched by deliveryid since order.orderid is not in the track
- // payload but deliveryid is the join key for /getdeliverylogs.
- const orderForTrack = focusedRider?.orders?.find(
- (o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
- );
+ const isFocusedStep = focusedCompareStep === t.sequenceStep;
+ const statusLow = String(t.orderstatus || '').toLowerCase();
+ const isDelivered = FINAL_STATUSES.has(statusLow);
+ const isSkipped = SKIPPED_STATUSES.has(statusLow);
+ const delta = compareDeltas.find((d) => d.sequenceStep === t.sequenceStep);
+ const isAnomaly = !!delta?.anomaly;
+ // Look up the corresponding order in the focused rider's set so
+ // the actual-track drop pin can render the same rich popup the
+ // planned-route number marker uses (Timeline, Details, KM chips).
+ // Matched by deliveryid since order.orderid is not in the track
+ // payload but deliveryid is the join key for /getdeliverylogs.
+ const orderForTrack = focusedRider?.orders?.find(
+ (o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
+ );
- const statusStyle = getStatusStyle(t.orderstatus);
- const flagSvg = t.orderstatus
- ? `
- {/* Compare-mode second pane — actual GPS tracks per delivery for the
+ {/* Compare-mode second pane — actual GPS tracks per delivery for the
focused rider. Fans out /deliveries/getdeliverylogs/?deliveryid=X
(one query per order, cached by deliveryid) and overlays each
track as a colored polyline + start/end markers. Renders alongside
#map-wrap when compareOpen so the operator can eyeball planned vs.
actual side by side. */}
- {compareOpen && focusedRider && (
-
- {/* Step timeline — every delivery as a tappable dot in chronological
+ {/* Step timeline — every delivery as a tappable dot in chronological
order. The operator can drill into any step to scrutinize that
single delivery on both maps. Filled = delivered, ring = pending,
dim = cancelled/skipped, ring with spinner = GPS still loading.
Whole strip (timeline + progress + legend) collapses via the
header chevron — open by default. */}
- {compareTimelineOpen && (
- <>
-
- {/* Legend strip — one line of icons so the operator instantly knows
+ {/* Legend strip — one line of icons so the operator instantly knows
what each line/marker means. Lives in the header so it doesn't
compete with the map for vertical real estate. */}
- {(() => {
- // Two color stories depending on view mode:
- // • Combined: polylines collapse to fixed indigo (planned)
- // and emerald (actual) so the two overlaid layers can be
- // told apart at a glance. Legend swatches mirror this.
- // • Planned-only / Actual-only: single layer on the map,
- // so polylines keep STEP_PALETTE and the swatch shows
- // the focused step's color or a step-gradient strip
- // (signals "varies by step").
- const isCombined = compareViewMode === 'combined';
- const stepSwatchBg = focusedDelta
- ? stepColor(focusedDelta.sequenceStep - 1)
- : `linear-gradient(90deg, ${STEP_PALETTE.slice(0, 6).join(', ')})`;
- const plannedSwatchBg = isCombined ? COMBINED_PLANNED_COLOR : stepSwatchBg;
- const actualSwatchBg = isCombined ? COMBINED_ACTUAL_COLOR : stepSwatchBg;
- return (
-
+ )}
- {/* Right-side data panel — full-height column in Compare mode. Renders
+ {/* Right-side data panel — full-height column in Compare mode. Renders
day-overview tiles (distance, deviation, on-time, profit), the
focused-step delta when a step is selected, a deviations list of
anomaly steps, and a per-step list showing whether each step was
@@ -4996,65 +4997,65 @@ const Dispatch = ({
vs expected, and the order profit. Clicking any step row mirrors
the timeline click — sets focusedCompareStep so both maps zoom
onto that delivery. */}
- {compareOpen && focusedRider && (
- setCompareOpen(false)}
- />
- )}
-
+ )}
- {/* Centered order popup — sibling of the map (NOT inside leaflet's
+ {/* Centered order popup — sibling of the map (NOT inside leaflet's
transformed panes) so position: fixed actually pins it to the
viewport. Replaces the marker-attached leaflet Popup so the rich
order card stays fully visible at the screen center on small
laptop displays. */}
- {centerPopupOrder && (
-