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))