updates on the active section and the dispatch page section regarding the active order details
This commit is contained in:
@@ -26,9 +26,7 @@ const ActiveSection = ({
|
||||
}) => {
|
||||
// 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')
|
||||
.flatMap((r) => (r.orders || []).filter((o) => String(o.orderstatus || '').toLowerCase() === 'active'))
|
||||
.sort((a, b) => {
|
||||
const mA = calculateEstMeters(a.rider_id || a.userid, a) ?? Infinity;
|
||||
const mB = calculateEstMeters(b.rider_id || b.userid, b) ?? Infinity;
|
||||
|
||||
@@ -1304,7 +1304,7 @@ const Dispatch = ({
|
||||
hasNextPage: liveHasNextPage,
|
||||
isFetchingNextPage: liveIsFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['dispatchDeliveries', selectedAppLocationId, liveUserid, 'all', selectedDate, selectedDate, 50, '', 0, 0, 0],
|
||||
queryKey: ['dispatchDeliveries', selectedAppLocationId, liveUserid, 'all', selectedDate, selectedDate, 1000, '', 0, 0, 0],
|
||||
queryFn: fetchDeliveries,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
|
||||
enabled: shouldFetchLive,
|
||||
@@ -1578,10 +1578,8 @@ const Dispatch = ({
|
||||
// excluded here. Every other view (By Location / By Zone / By Rider) is
|
||||
// unchanged.
|
||||
const isAllActiveView = viewMode === 'all';
|
||||
// Orders belonging to GPS-active riders — the candidate pool for this view's
|
||||
// list, drop set and auto-fit. Narrowed to in-progress orders below.
|
||||
const allViewOrders = useMemo(
|
||||
() => (isAllActiveView ? allOrders.filter((o) => activeRiderIdSet.has(String(o.rider_id))) : allOrders),
|
||||
() => (isAllActiveView ? allOrders.filter((o) => activeRiderIdSet.has(String(o.rider_id || o.userid || ''))) : allOrders),
|
||||
[isAllActiveView, allOrders, activeRiderIdSet]
|
||||
);
|
||||
// The single gate for the whole Active view: rider ids that are GPS-active AND
|
||||
@@ -1592,8 +1590,8 @@ const Dispatch = ({
|
||||
() =>
|
||||
new Set(
|
||||
(isAllActiveView ? allViewOrders : [])
|
||||
.filter(isActiveDelivery)
|
||||
.map((o) => String(o.rider_id))
|
||||
.filter((o) => String(o?.orderstatus || '').toLowerCase() === 'active')
|
||||
.map((o) => String(o.rider_id || o.userid || ''))
|
||||
),
|
||||
[isAllActiveView, allViewOrders]
|
||||
);
|
||||
@@ -2305,8 +2303,12 @@ const Dispatch = ({
|
||||
if (focusedRider && focusedRider.id !== r.id) return;
|
||||
if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
|
||||
|
||||
const activeOrder = isAllActiveView ? getActiveOrder(r.orders) : null;
|
||||
const isActiveAndShown = activeOrder && String(activeOrder.orderstatus || '').toLowerCase() === 'active';
|
||||
const rOrders = isAllActiveView ? (isActiveAndShown ? [activeOrder] : []) : r.orders;
|
||||
|
||||
const trips = {};
|
||||
r.orders.forEach(o => {
|
||||
rOrders.forEach(o => {
|
||||
const t = o.trip_number || 1;
|
||||
if (!trips[t]) trips[t] = [];
|
||||
trips[t].push(o);
|
||||
@@ -2320,12 +2322,27 @@ const Dispatch = ({
|
||||
|
||||
if (filteredTOrders.length === 0) return;
|
||||
|
||||
const cacheKey = `${r.id}-${tNum}`;
|
||||
const cacheKey = isAllActiveView ? `${r.id}-active-${filteredTOrders[0].orderid}` : `${r.id}-${tNum}`;
|
||||
const roadPath = osrmRoutes[cacheKey];
|
||||
const sorted = [...filteredTOrders].sort((a, b) => (a.step || 0) - (b.step || 0));
|
||||
|
||||
// Aerial fallback — NaN-safe build
|
||||
const aerialPath = buildTripPoints(sorted);
|
||||
// Aerial fallback
|
||||
let activeStraightLeg = null;
|
||||
if (isAllActiveView && filteredTOrders[0]) {
|
||||
const o = filteredTOrders[0];
|
||||
const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
|
||||
const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
|
||||
? [lp.lat, lp.lon]
|
||||
: (hasValidPickup(o)
|
||||
? [toNum(pickupLat(o)), toNum(pickupLon(o))]
|
||||
: null);
|
||||
const dLat = toNum(o.droplat || o.deliverylat);
|
||||
const dLon = toNum(o.droplon || o.deliverylong);
|
||||
if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
|
||||
activeStraightLeg = [start, [dLat, dLon]];
|
||||
}
|
||||
}
|
||||
const aerialPath = isAllActiveView ? (activeStraightLeg || []) : buildTripPoints(sorted);
|
||||
|
||||
const isKitchenAerial = (viewMode === 'kitchens' || focusedKitchen);
|
||||
const path = roadPath || aerialPath;
|
||||
@@ -3997,177 +4014,177 @@ const Dispatch = ({
|
||||
})
|
||||
: tOrders;
|
||||
return (
|
||||
<div key={tNum} className="trip-block">
|
||||
<div className="trip-header" style={{ background: `${focusedRider.color}12`, borderColor: `${focusedRider.color}30` }}>
|
||||
<span className="th-badge" style={{ background: focusedRider.color }}>Trip {tNum}</span>
|
||||
<span className="trip-stats">
|
||||
<span><Ico><MdLocationOn /></Ico>{tOrders.length} stops</span>
|
||||
<span><Ico><MdStraighten /></Ico>{tOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span>
|
||||
</span>
|
||||
{/* iOS-style segmented control: Planned (dispatched step
|
||||
<div key={tNum} className="trip-block">
|
||||
<div className="trip-header" style={{ background: `${focusedRider.color}12`, borderColor: `${focusedRider.color}30` }}>
|
||||
<span className="th-badge" style={{ background: focusedRider.color }}>Trip {tNum}</span>
|
||||
<span className="trip-stats">
|
||||
<span><Ico><MdLocationOn /></Ico>{tOrders.length} stops</span>
|
||||
<span><Ico><MdStraighten /></Ico>{tOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span>
|
||||
</span>
|
||||
{/* iOS-style segmented control: Planned (dispatched step
|
||||
order) vs By time (completion order). The active item is
|
||||
a white "thumb"; the rider color stays on the Trip badge
|
||||
so the pills don't compete with it. */}
|
||||
<div
|
||||
className="trip-sort-toggle"
|
||||
role="group"
|
||||
aria-label="Sort stops by"
|
||||
data-mode={isTimeMode ? 'time' : 'planned'}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`trip-sort-pill ${!isTimeMode ? 'is-active' : ''}`}
|
||||
aria-pressed={!isTimeMode}
|
||||
onClick={() => setTripSortMode('planned')}
|
||||
title="Sort stops by planned step (dispatched order)"
|
||||
<div
|
||||
className="trip-sort-toggle"
|
||||
role="group"
|
||||
aria-label="Sort stops by"
|
||||
data-mode={isTimeMode ? 'time' : 'planned'}
|
||||
>
|
||||
<MdFormatListBulleted aria-hidden="true" />
|
||||
<span>Planned</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`trip-sort-pill ${isTimeMode ? 'is-active' : ''}`}
|
||||
aria-pressed={isTimeMode}
|
||||
onClick={() => setTripSortMode('time')}
|
||||
title="Sort stops by completion time (which delivery was done first)"
|
||||
>
|
||||
<MdAccessTime aria-hidden="true" />
|
||||
<span>By time</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`trip-sort-pill ${!isTimeMode ? 'is-active' : ''}`}
|
||||
aria-pressed={!isTimeMode}
|
||||
onClick={() => setTripSortMode('planned')}
|
||||
title="Sort stops by planned step (dispatched order)"
|
||||
>
|
||||
<MdFormatListBulleted aria-hidden="true" />
|
||||
<span>Planned</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`trip-sort-pill ${isTimeMode ? 'is-active' : ''}`}
|
||||
aria-pressed={isTimeMode}
|
||||
onClick={() => setTripSortMode('time')}
|
||||
title="Sort stops by completion time (which delivery was done first)"
|
||||
>
|
||||
<MdAccessTime aria-hidden="true" />
|
||||
<span>By time</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="zone-order-grid">
|
||||
{displayOrders.map((o, idx) => {
|
||||
const kitchenKey = (o.kitchen_key || o.pickupcustomer || 'Unknown').toLowerCase().trim();
|
||||
const showTransition = prevKitchenKey !== null && kitchenKey !== prevKitchenKey;
|
||||
prevKitchenKey = kitchenKey;
|
||||
const isStopActive = focusedStop && focusedStop.orderid === o.orderid;
|
||||
const isGoingOn = activeOrderId && o.orderid === activeOrderId;
|
||||
const lat = parseFloat(o.droplat || o.deliverylat);
|
||||
const lon = parseFloat(o.droplon || o.deliverylong);
|
||||
const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
|
||||
const statusStyle = getStatusStyle(o.orderstatus);
|
||||
const estMeters = calculateEstMeters(focusedRider.id, o);
|
||||
// In "By time" mode, stops with no actual delivery time
|
||||
// were sunk to the bottom — dim them so it's obvious
|
||||
// they're not yet delivered without reading the status.
|
||||
const isUndeliveredInTimeMode = isTimeMode && !o.deliverytime;
|
||||
|
||||
return (
|
||||
<React.Fragment key={o.orderid}>
|
||||
{showTransition && (
|
||||
<div className="kitchen-transition"><span className="kt-ico"><MdSwapHoriz /></span> Switch to <strong>{o.pickupcustomer}</strong></div>
|
||||
)}
|
||||
<div
|
||||
className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''} ${isGoingOn ? 'going-on' : ''} ${isUndeliveredInTimeMode ? 'is-pending-time' : ''}`}
|
||||
role={canFocus ? 'button' : undefined}
|
||||
tabIndex={canFocus ? 0 : undefined}
|
||||
onClick={canFocus ? () => setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
|
||||
onKeyDown={canFocus ? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon });
|
||||
}
|
||||
} : undefined}
|
||||
title={canFocus ? (isStopActive ? 'Click to show full trip' : `Show ${o.deliverycustomer || `order #${o.orderid}`} on map`) : undefined}
|
||||
>
|
||||
<div className="zone-order-card-head">
|
||||
<div className="zone-order-num">{o.step || idx + 1}</div>
|
||||
<div className="zone-order-id-block">
|
||||
<div className="zone-order-id">Order #{o.orderid}</div>
|
||||
</div>
|
||||
{(() => {
|
||||
// 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 (
|
||||
<div className="zone-order-status-stack">
|
||||
{o.orderstatus && (
|
||||
<span
|
||||
className="zone-order-status"
|
||||
style={{ background: statusStyle.bg, color: statusStyle.fg }}
|
||||
>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
)}
|
||||
{(actual || expected) && (
|
||||
<span
|
||||
className={`zone-order-time ${actual ? '' : 'is-expected'}`}
|
||||
title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
|
||||
>
|
||||
<MdAccessTime />{actual || expected}
|
||||
</span>
|
||||
)}
|
||||
{showEstDrop && (
|
||||
<span
|
||||
className="zone-order-est-drop"
|
||||
title="Estimated distance to drop location"
|
||||
>
|
||||
<MdMyLocation />{formatMeters(estMeters)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{onChangeRider && (
|
||||
<button
|
||||
type="button"
|
||||
className="zone-order-change-rider"
|
||||
title="Change rider"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChangeRider(o, focusedRider);
|
||||
}}
|
||||
>
|
||||
<MdSwapHoriz />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="zone-order-customer">
|
||||
<Ico><MdMarkunreadMailbox /></Ico>{o.deliverycustomer || '—'}
|
||||
</div>
|
||||
|
||||
{o.pickupcustomer && (
|
||||
<div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
|
||||
<Ico><MdRestaurant /></Ico>{o.pickupcustomer}
|
||||
</div>
|
||||
)}
|
||||
{(o.deliverysuburb || o.deliveryaddress) && (
|
||||
<div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
|
||||
<Ico><MdLocationOn /></Ico>{o.deliverysuburb || extractArea(o.deliveryaddress)}
|
||||
</div>
|
||||
)}
|
||||
{o.ordernotes && (
|
||||
<div className="zone-order-line zone-order-notes" title={o.ordernotes}>
|
||||
<Ico><MdNotes /></Ico>{o.ordernotes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="zone-order-stats">
|
||||
<span className="zone-order-chip" title="Distance">
|
||||
<Ico><MdStraighten /></Ico>{o.actualkms || o.kms || 0} km
|
||||
</span>
|
||||
{o.ordertype && (
|
||||
<span className={`zone-order-chip zone-order-type type-${String(o.ordertype).toLowerCase()}`}>
|
||||
{o.ordertype}
|
||||
</span>
|
||||
)}
|
||||
<span className="zone-order-chip zone-order-trip">
|
||||
T{o.trip_number || tNum} · S{o.step || idx + 1}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="zone-order-grid">
|
||||
{displayOrders.map((o, idx) => {
|
||||
const kitchenKey = (o.kitchen_key || o.pickupcustomer || 'Unknown').toLowerCase().trim();
|
||||
const showTransition = prevKitchenKey !== null && kitchenKey !== prevKitchenKey;
|
||||
prevKitchenKey = kitchenKey;
|
||||
const isStopActive = focusedStop && focusedStop.orderid === o.orderid;
|
||||
const isGoingOn = activeOrderId && o.orderid === activeOrderId;
|
||||
const lat = parseFloat(o.droplat || o.deliverylat);
|
||||
const lon = parseFloat(o.droplon || o.deliverylong);
|
||||
const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
|
||||
const statusStyle = getStatusStyle(o.orderstatus);
|
||||
const estMeters = calculateEstMeters(focusedRider.id, o);
|
||||
// In "By time" mode, stops with no actual delivery time
|
||||
// were sunk to the bottom — dim them so it's obvious
|
||||
// they're not yet delivered without reading the status.
|
||||
const isUndeliveredInTimeMode = isTimeMode && !o.deliverytime;
|
||||
|
||||
return (
|
||||
<React.Fragment key={o.orderid}>
|
||||
{showTransition && (
|
||||
<div className="kitchen-transition"><span className="kt-ico"><MdSwapHoriz /></span> Switch to <strong>{o.pickupcustomer}</strong></div>
|
||||
)}
|
||||
<div
|
||||
className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''} ${isGoingOn ? 'going-on' : ''} ${isUndeliveredInTimeMode ? 'is-pending-time' : ''}`}
|
||||
role={canFocus ? 'button' : undefined}
|
||||
tabIndex={canFocus ? 0 : undefined}
|
||||
onClick={canFocus ? () => setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
|
||||
onKeyDown={canFocus ? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon });
|
||||
}
|
||||
} : undefined}
|
||||
title={canFocus ? (isStopActive ? 'Click to show full trip' : `Show ${o.deliverycustomer || `order #${o.orderid}`} on map`) : undefined}
|
||||
>
|
||||
<div className="zone-order-card-head">
|
||||
<div className="zone-order-num">{o.step || idx + 1}</div>
|
||||
<div className="zone-order-id-block">
|
||||
<div className="zone-order-id">Order #{o.orderid}</div>
|
||||
</div>
|
||||
{(() => {
|
||||
// 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 (
|
||||
<div className="zone-order-status-stack">
|
||||
{o.orderstatus && (
|
||||
<span
|
||||
className="zone-order-status"
|
||||
style={{ background: statusStyle.bg, color: statusStyle.fg }}
|
||||
>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
)}
|
||||
{(actual || expected) && (
|
||||
<span
|
||||
className={`zone-order-time ${actual ? '' : 'is-expected'}`}
|
||||
title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
|
||||
>
|
||||
<MdAccessTime />{actual || expected}
|
||||
</span>
|
||||
)}
|
||||
{showEstDrop && (
|
||||
<span
|
||||
className="zone-order-est-drop"
|
||||
title="Estimated distance to drop location"
|
||||
>
|
||||
<MdMyLocation />{formatMeters(estMeters)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{onChangeRider && (
|
||||
<button
|
||||
type="button"
|
||||
className="zone-order-change-rider"
|
||||
title="Change rider"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChangeRider(o, focusedRider);
|
||||
}}
|
||||
>
|
||||
<MdSwapHoriz />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="zone-order-customer">
|
||||
<Ico><MdMarkunreadMailbox /></Ico>{o.deliverycustomer || '—'}
|
||||
</div>
|
||||
|
||||
{o.pickupcustomer && (
|
||||
<div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
|
||||
<Ico><MdRestaurant /></Ico>{o.pickupcustomer}
|
||||
</div>
|
||||
)}
|
||||
{(o.deliverysuburb || o.deliveryaddress) && (
|
||||
<div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
|
||||
<Ico><MdLocationOn /></Ico>{o.deliverysuburb || extractArea(o.deliveryaddress)}
|
||||
</div>
|
||||
)}
|
||||
{o.ordernotes && (
|
||||
<div className="zone-order-line zone-order-notes" title={o.ordernotes}>
|
||||
<Ico><MdNotes /></Ico>{o.ordernotes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="zone-order-stats">
|
||||
<span className="zone-order-chip" title="Distance">
|
||||
<Ico><MdStraighten /></Ico>{o.actualkms || o.kms || 0} km
|
||||
</span>
|
||||
{o.ordertype && (
|
||||
<span className={`zone-order-chip zone-order-type type-${String(o.ordertype).toLowerCase()}`}>
|
||||
{o.ordertype}
|
||||
</span>
|
||||
)}
|
||||
<span className="zone-order-chip zone-order-trip">
|
||||
T{o.trip_number || tNum} · S{o.step || idx + 1}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user