import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { MdTwoWheeler, MdLocationOn, MdRestaurant, MdMyLocation, MdAccessTime, MdInventory2 } from 'react-icons/md'; import { List, ListItem } from '@astryxdesign/core/List'; import { VStack } from '@astryxdesign/core/VStack'; import { HStack } from '@astryxdesign/core/HStack'; import { Center } from '@astryxdesign/core/Center'; import { Text } from '@astryxdesign/core/Text'; import { EmptyState } from '@astryxdesign/core/EmptyState'; import { Tooltip } from '@astryxdesign/core/Tooltip'; import { parseDoormileTimestamp } from 'utils/doormileTimestamp'; import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; import StatusBadge from 'components/nearle_components/StatusBadge'; import { AccentAvatar } from 'themes/dt/primitives'; import { getActiveOrder } from './dispatchShared'; import { OpenToast } from 'components/third-party/OpenToast'; // ==============================|| ACTIVE DELIVERIES (sidebar) ||============================== // // Rows, not cards. Astryx's design rule is that dense operator data reads as // edge-to-edge rows — the old `.adcard` bordered cards cost ~16px of chrome per // delivery, which is a lot of scrolling once a slot has 20+ riders out. // // Status comes from (themes/dt/status.js), which is now the only // status-colour table in this folder — dispatchShared's rival copy has been // deleted and its getStatusStyle reduced to an adapter over the same source. // Small labelled metric — icon + value, used for the distance/ETA pair. // The old markup carried a native `title` for the label ("Distance to drop"); // Astryx's BaseProps deliberately omits `title`, so the affordance moves to the // real rather than silently disappearing onto a dropped attribute. const Metric = ({ icon, children, label }) => (
{icon}
{children}
); Metric.propTypes = { icon: PropTypes.node, children: PropTypes.node, label: PropTypes.string }; // One line of secondary context (rider / drop area / pickup) under the customer. // `maxLines={1}` turns on Text's own truncate tooltip, which replaces the // `title` the old spans used to show the full value when clipped. const ContextLine = ({ icon, children }) => (
{icon}
{children}
); ContextLine.propTypes = { icon: PropTypes.node, children: PropTypes.node }; const ActiveSection = ({ visibleRiders, riders, focusedStop, handleRiderFocus, setFocusedStop, calculateEstMeters, getRiderColor, formatMeters, isLoading }) => { // Sort by live distance ascending (closest drop-off first; null/unknown last) const activeDeliveries = visibleRiders .map((r) => getActiveOrder(r.orders)) .filter(Boolean) .sort((a, b) => { const mA = calculateEstMeters(a.rider_id || a.userid, a) ?? Infinity; const mB = calculateEstMeters(b.rider_id || b.userid, 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) { // Brand-new order that is already active → rider has taken a new order if (cur.status === 'active') { OpenToast(`🛵 ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000); } return; } // Rider transitioned into active → started heading to customer if (old.status !== 'active' && cur.status === 'active') { OpenToast(`🛵 ${cur.riderName} is now delivering to ${cur.customer}`, 'success', 3000); } // Rider delivered → reached customer location 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 ( } /> ); } const renderActiveDelivery = (o) => { const rid = o.rider_id || o.userid; const rider = riders.find((r) => String(r.id) === String(rid)); const color = getRiderColor(rid); 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 isSelected = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid); const initials = riderName .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((w) => w[0]) .join('') .toUpperCase() || '•'; const deliveredAt = o.deliverytime ? parseDoormileTimestamp(o.deliverytime).isValid() ? parseDoormileTimestamp(o.deliverytime).format('HH:mm:ss') : String(o.deliverytime) : null; // 20 km/h is the same city-average assumption the old card used. const etaMin = estMeters !== null ? (estMeters / 1000 / 20) * 60 : null; return ( { if (rider) handleRiderFocus(rider); if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon }); }} startContent={ // Rider colour is an arbitrary per-rider hex assigned by the map's // palette, which no semantic token can express — this is the same // caller-supplied-accent escape hatch StatCard uses. {initials} } description={ }>{riderName} {dropArea && }>{dropArea}} {o.pickupcustomer && }>{o.pickupcustomer}} } endContent={ {deliveredAt && ( {deliveredAt} )} {estMeters !== null && ( } label="Distance to drop"> {formatMeters(estMeters)} } label="Estimated time to drop"> {etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`} )} } /> ); }; return ( {activeDeliveries.map(renderActiveDelivery)} ); }; export default ActiveSection;