diff --git a/src/pages/nearle/dispatch/ActiveSection.js b/src/pages/nearle/dispatch/ActiveSection.js
new file mode 100644
index 0000000..46489d5
--- /dev/null
+++ b/src/pages/nearle/dispatch/ActiveSection.js
@@ -0,0 +1,195 @@
+import React, { useRef, useEffect } from 'react';
+import {
+ MdTwoWheeler,
+ MdLocationOn,
+ MdRestaurant,
+ MdMyLocation,
+ MdAccessTime,
+ MdInventory2
+} from 'react-icons/md';
+import dayjs from 'dayjs';
+import { CircularProgress } from '@mui/material';
+import { getStatusStyle, getActiveOrder } from './dispatchShared';
+import { OpenToast } from 'components/nearle_components/OpenToast';
+
+const ActiveSection = ({
+ visibleRiders,
+ riders,
+ focusedStop,
+ handleRiderFocus,
+ setFocusedStop,
+ calculateEstMeters,
+ getRiderColor,
+ formatMeters,
+ isLoading,
+ onMapZoom
+}) => {
+ // Sort by live distance ascending (closest drop-off first; null/unknown last)
+ const activeDeliveries = visibleRiders
+ .map((r) => getActiveOrder(r.orders))
+ .filter(Boolean)
+ .filter((o) => String(o.orderstatus || '').toLowerCase() === 'active')
+ .sort((a, b) => {
+ const mA = calculateEstMeters(a.rider_id, a) ?? Infinity;
+ const mB = calculateEstMeters(b.rider_id, b) ?? Infinity;
+ return mA - mB;
+ });
+
+ // Toast notifications: detect status transitions across renders
+ const prevStatusMapRef = useRef(null);
+
+ useEffect(() => {
+ const currentMap = {};
+ visibleRiders.forEach((r) => {
+ (r.orders || []).forEach((o) => {
+ currentMap[String(o.orderid)] = {
+ status: String(o.orderstatus || '').toLowerCase(),
+ riderName: o.rider_name || o.ridername || 'Rider',
+ customer: o.deliverycustomer || o.customername || `Order #${o.orderid}`
+ };
+ });
+ });
+
+ const prev = prevStatusMapRef.current;
+ if (prev !== null) {
+ Object.entries(currentMap).forEach(([oid, cur]) => {
+ const old = prev[oid];
+ if (!old) {
+ if (cur.status === 'active') {
+ OpenToast(`๐ต ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000);
+ }
+ return;
+ }
+ if (old.status !== 'active' && cur.status === 'active') {
+ OpenToast(`๐ต ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000);
+ }
+ if (old.status === 'active' && cur.status === 'delivered') {
+ OpenToast(`๐ ${cur.riderName} has reached ${cur.customer}'s location`, 'info', 3000);
+ }
+ });
+ }
+
+ prevStatusMapRef.current = currentMap;
+ }, [visibleRiders]);
+
+ if (isLoading) {
+ return (
+
+
+
Loading active deliveries...
+
+ );
+ }
+
+ if (activeDeliveries.length === 0) {
+ return (
+
+
+
+
+
No active deliveries
+
+ No deliveries are currently in progress for this slot
+
+
+ );
+ }
+
+ const renderActiveDeliveryCard = (o, i) => {
+ const rid = o.rider_id;
+ const rider = riders.find((r) => String(r.id) === String(rid));
+ const color = getRiderColor(rid);
+ const statusStyle = getStatusStyle(o.orderstatus);
+ const lat = parseFloat(o.droplat || o.deliverylat);
+ const lon = parseFloat(o.droplon || o.deliverylong);
+ const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
+ const estMeters = calculateEstMeters(rid, o);
+ const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`;
+ const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || '';
+ const riderName = o.rider_name || o.ridername || 'Unassigned';
+ const isActive = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid);
+ const initials =
+ riderName
+ .split(/\s+/)
+ .filter(Boolean)
+ .slice(0, 2)
+ .map((w) => w[0])
+ .join('')
+ .toUpperCase() || 'โข';
+
+ return (
+ {
+ if (canFocus && onMapZoom) onMapZoom(lat, lon);
+ }}
+ >
+
+
{initials}
+
+
+
+ {riderName}
+
+
{customer}
+
+
+
+ {statusStyle.label}
+
+ {o.deliverytime && (
+
+ {dayjs(o.deliverytime).isValid() ? dayjs(o.deliverytime).format('HH:mm:ss') : String(o.deliverytime)}
+
+ )}
+
+
+
+ {dropArea && (
+
+
+ {dropArea}
+
+ )}
+
+
+ {o.pickupcustomer ? (
+
+
+ {o.pickupcustomer}
+
+ ) : (
+
+ )}
+
+ {estMeters !== null && (
+ <>
+
+
+ {formatMeters(estMeters)}
+
+
+
+ {(() => {
+ const etaMin = estMeters / 1000 / 20 * 60;
+ return etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`;
+ })()}
+
+ >
+ )}
+
+
+
+ );
+ };
+
+ return {activeDeliveries.map(renderActiveDeliveryCard)}
;
+};
+
+export default ActiveSection;
diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css
index 496b00e..7f1f35d 100644
--- a/src/pages/nearle/dispatch/Dispatch.css
+++ b/src/pages/nearle/dispatch/Dispatch.css
@@ -2193,9 +2193,9 @@
}
.dispatch-container .adcard-customer {
- font-size: 14.5px;
- font-weight: 700;
- color: var(--text);
+ font-size: 11.5px;
+ font-weight: 600;
+ color: var(--text-muted);
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
@@ -2206,9 +2206,9 @@
display: flex;
align-items: center;
gap: 5px;
- font-size: 12px;
- font-weight: 600;
- color: var(--text-muted);
+ font-size: 14.5px;
+ font-weight: 700;
+ color: var(--text);
margin-top: 2px;
min-width: 0;
}
diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js
index f6488da..321d37c 100644
--- a/src/pages/nearle/dispatch/Dispatch.js
+++ b/src/pages/nearle/dispatch/Dispatch.js
@@ -63,6 +63,7 @@ import {
stepColor
} from './dispatchShared';
import CompareDataPanel from './CompareDataPanel';
+import ActiveSection from './ActiveSection';
import './Dispatch.css';
import logger from '../../../utils/logger';
@@ -482,8 +483,13 @@ const getStableRiderColor = (id) => {
// extracted CompareDataPanel component can import them without forcing
// a circular dependency on Dispatch.js.
-const MapController = ({ focusedItem, viewMode, orders, kitchens, locationKey, extraPoints }) => {
+const MapController = ({ focusedItem, viewMode, orders, kitchens, locationKey, extraPoints, flyTo }) => {
const map = useMap();
+
+ useEffect(() => {
+ if (!flyTo) return;
+ map.setView([flyTo.lat, flyTo.lon], 16, { animate: true, duration: 0.6 });
+ }, [flyTo, map]);
// Last fit signature. We only call fitBounds when this changes โ otherwise
// every parent render (data refetch, sidebar tick, etc.) would refit the
// map and snap it back mid-drag, which felt like the map was un-draggable.
@@ -831,6 +837,8 @@ const Dispatch = ({
const [focusedZone, setFocusedZone] = useState(null);
// Single delivery stop pinned by clicking its sidebar row โ overrides the rider's full-route bounds on the map.
const [focusedStop, setFocusedStop] = useState(null);
+ // Fly-to target set by ActiveSection card clicks โ zooms the map without touching sidebar or popup.
+ const [activeFlyTo, setActiveFlyTo] = useState(null);
// How the stops inside each trip block are ordered in the focused-rider sidebar:
// 'planned' โ the dispatched route order (by step) โ the default.
// 'time' โ re-sorted by when each delivery was actually completed, so the
@@ -2541,96 +2549,6 @@ const Dispatch = ({
// operators monitor the work, not the people. Clicking focuses the owning
// rider and centers the map on the drop (which, per the Active-view rules,
// collapses to that rider's single active leg + drop pin).
- const renderActiveDeliveryCard = (o, i) => {
- const rid = o.rider_id;
- const rider = riders.find((r) => String(r.id) === String(rid));
- const color = getRiderColor(rid);
- const statusStyle = getStatusStyle(o.orderstatus);
- const lat = parseFloat(o.droplat || o.deliverylat);
- const lon = parseFloat(o.droplon || o.deliverylong);
- const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
- const estMeters = calculateEstMeters(rid, o);
- const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`;
- const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || '';
- const riderName = o.rider_name || o.ridername || 'Unassigned';
- // This card is "active" (popped on the map) when its drop is the focused stop.
- const isActive = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid);
- // Up-to-two-letter rider initials for the avatar.
- const initials =
- riderName
- .split(/\s+/)
- .filter(Boolean)
- .slice(0, 2)
- .map((w) => w[0])
- .join('')
- .toUpperCase() || 'โข';
-
- return (
- {
- if (rider) handleRiderFocus(rider);
- if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
- }}
- >
-
-
{initials}
-
-
{customer}
-
-
- {riderName}
-
-
-
- {statusStyle.label}
-
-
-
- {dropArea && (
-
-
- {dropArea}
-
- )}
-
-
- {o.pickupcustomer ? (
-
-
- {o.pickupcustomer}
-
- ) : (
-
- )}
-
- {estMeters !== null && (
- <>
-
-
- {formatMeters(estMeters)}
-
-
-
- {(() => {
- const etaMin = estMeters / 1000 / 20 * 60;
- return etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`;
- })()}
-
- >
- )}
-
-
-
- );
- };
-
// Returns true when the order's centered popup should stay open even after
// the cursor leaves the marker: either explicitly pinned via click, or the
// matching compare-step is focused (so clicking a step in the right panel
@@ -2750,6 +2668,17 @@ const Dispatch = ({
)}
+ {o.step != null && (
+
+
+
+
Step
+
+ {o.trip_number != null && o.trip_number > 1 ? `Trip ${o.trip_number} ยท ` : ''}#{o.step}
+
+
+
+ )}
{(o.actualkms != null || (!isDelivered && o.riderkms != null) || estMeters !== null) && (
@@ -4645,43 +4574,18 @@ const Dispatch = ({
))
) : isAllActiveView ? (
- // Active view: list exactly ONE card per active rider โ the
- // single in-progress leg (getActiveOrder) the map draws a
- // route + destination flag for. Driving the list off the
- // same `visibleRiders` set the map uses keeps the sidebar
- // and the map in lock-step (same count, same deliveries).
- (() => {
- if (shouldFetchLive && liveIsFetching && visibleRiders.length === 0) {
- return (
-
-
-
Loading active deliveries...
-
- );
- }
- const activeDeliveries = visibleRiders
- .map((r) => getActiveOrder(r.orders))
- .filter(Boolean)
- .sort((a, b) =>
- String(a.rider_name || a.ridername || '').localeCompare(
- String(b.rider_name || b.ridername || '')
- )
- );
- if (activeDeliveries.length === 0) {
- return (
-
-
-
-
-
No active deliveries
-
- No deliveries are currently in progress for this slot
-
-
- );
- }
- return {activeDeliveries.map(renderActiveDeliveryCard)}
;
- })()
+ setActiveFlyTo({ lat, lon, t: Date.now() })}
+ />
) : (
visibleRiders.map(renderRiderCard)
)}
@@ -4708,7 +4612,7 @@ const Dispatch = ({
{compareOpen && }
-
+
{kitchens
.filter(k => Number.isFinite(k.lat) && Number.isFinite(k.lon))
.filter(k => !focusedRider || k.riders.has(focusedRider.id))