updates on the console under the dispatch page active section
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
MdInventory2
|
||||
} from 'react-icons/md';
|
||||
import dayjs from 'dayjs';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { getStatusStyle, getActiveOrder } from './dispatchShared';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
|
||||
@@ -19,15 +20,16 @@ const ActiveSection = ({
|
||||
setFocusedStop,
|
||||
calculateEstMeters,
|
||||
getRiderColor,
|
||||
formatMeters
|
||||
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) ?? Infinity;
|
||||
const mB = calculateEstMeters(b.rider_id, b) ?? Infinity;
|
||||
const mA = calculateEstMeters(a.rider_id || a.userid, a) ?? Infinity;
|
||||
const mB = calculateEstMeters(b.rider_id || b.userid, b) ?? Infinity;
|
||||
return mA - mB;
|
||||
});
|
||||
|
||||
@@ -71,6 +73,15 @@ const ActiveSection = ({
|
||||
prevStatusMapRef.current = currentMap;
|
||||
}, [visibleRiders]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="empty-slot" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', minHeight: '200px' }}>
|
||||
<CircularProgress size={30} style={{ color: '#7b1fa2', marginBottom: '16px' }} />
|
||||
<div className="empty-slot-title">Loading active deliveries...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeDeliveries.length === 0) {
|
||||
return (
|
||||
<div className="empty-slot">
|
||||
@@ -86,7 +97,7 @@ const ActiveSection = ({
|
||||
}
|
||||
|
||||
const renderActiveDeliveryCard = (o, i) => {
|
||||
const rid = o.rider_id;
|
||||
const rid = o.rider_id || o.userid;
|
||||
const rider = riders.find((r) => String(r.id) === String(rid));
|
||||
const color = getRiderColor(rid);
|
||||
const statusStyle = getStatusStyle(o.orderstatus);
|
||||
@@ -111,7 +122,7 @@ const ActiveSection = ({
|
||||
<div
|
||||
key={o.orderid}
|
||||
className={`adcard${isActive ? ' is-active' : ''}`}
|
||||
style={{ '--ad-accent': color, animationDelay: `${i * 0.05}s` }}
|
||||
style={{ '--ad-accent': color }}
|
||||
onClick={() => {
|
||||
if (rider) handleRiderFocus(rider);
|
||||
if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
|
||||
|
||||
@@ -10754,13 +10754,23 @@
|
||||
padding: 4px 0 16px;
|
||||
}
|
||||
|
||||
@keyframes adcard-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.dispatch-container .adcard {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* Explicit properties so the CSS animation (adcard-in) isn't interrupted by transition: all */
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: adcard-in 0.28s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1613,6 +1613,18 @@ const Dispatch = ({
|
||||
if (liveHasNextPage && !liveIsFetchingNextPage) liveFetchNextPage();
|
||||
}, [shouldFetchLive, liveHasNextPage, liveIsFetchingNextPage, liveFetchNextPage]);
|
||||
|
||||
// Holds the last fully-loaded row set (all pages present). Used below to
|
||||
// prevent the "orders vanish then reappear" flash during background refetches
|
||||
// where TanStack Query fetches pages sequentially and livePagesData temporarily
|
||||
// contains only page 1 while the rest are still loading.
|
||||
const prevCompleteRowsRef = useRef([]);
|
||||
|
||||
// Sticky cache for active orders in the Active Section sidebar. Keys are orderid strings.
|
||||
// Orders are added/refreshed when orderstatus === 'active' and evicted only when the
|
||||
// API explicitly returns a terminal status. This prevents cards from flashing away
|
||||
// during the 15-second poll cycle when the API temporarily omits a row.
|
||||
const stickyActiveRef = useRef({});
|
||||
|
||||
const liveRows = useMemo(() => {
|
||||
// Flatten infinite-query pages, then dedupe by orderid. The deliveries API
|
||||
// can return the same orderid more than once (e.g. when page bookkeeping
|
||||
@@ -1627,8 +1639,26 @@ const Dispatch = ({
|
||||
if (key) seen.add(key);
|
||||
out.push(r);
|
||||
}
|
||||
|
||||
// Background refetch loads pages one-by-one: page 1 arrives first, then the
|
||||
// auto-pager fetches page 2, 3… While pages are still loading (liveHasNextPage
|
||||
// is true and a fetch is in-flight), "out" is a partial snapshot that is
|
||||
// smaller than the complete dataset we had before. Returning prevCompleteRowsRef
|
||||
// keeps the Active section stable — no cards disappear mid-refetch.
|
||||
// On the very first load prevCompleteRowsRef is empty, so we fall through and
|
||||
// show whatever partial data is already available (correct for initial render).
|
||||
if (liveIsFetching && liveHasNextPage && prevCompleteRowsRef.current.length > 0) {
|
||||
return prevCompleteRowsRef.current;
|
||||
}
|
||||
|
||||
// All pages have arrived — this is a complete snapshot. Persist it so the
|
||||
// next refetch cycle can return it during the partial-pages window above.
|
||||
if (!liveHasNextPage) {
|
||||
prevCompleteRowsRef.current = out;
|
||||
}
|
||||
|
||||
return out;
|
||||
}, [livePagesData]);
|
||||
}, [livePagesData, liveIsFetching, liveHasNextPage]);
|
||||
|
||||
// Distinct riders across the WHOLE day's rows — NOT slot-filtered. Used by
|
||||
// the Rider Info view so the operator can pick any rider regardless of
|
||||
@@ -1957,6 +1987,56 @@ const Dispatch = ({
|
||||
: riders,
|
||||
[isAllActiveView, riders, activeOrderRiderIdSet]
|
||||
);
|
||||
// Active section sidebar riders — built from the FULL day's unfiltered rows
|
||||
// (liveRows, all batches, no GPS-status gate). Uses a sticky cache so cards
|
||||
// only disappear when the API returns an explicit terminal status (delivered /
|
||||
// cancelled / skipped). Temporary row omissions during the 15-second poll
|
||||
// cycle (single-page refetch, brief API inconsistency) no longer cause cards
|
||||
// to flash away and reappear.
|
||||
const activeViewRiders = useMemo(() => {
|
||||
if (!isAllActiveView) {
|
||||
stickyActiveRef.current = {};
|
||||
return [];
|
||||
}
|
||||
|
||||
const TERMINAL = new Set(['delivered', 'cancelled', 'skipped', 'complete', 'completed']);
|
||||
|
||||
liveRows.forEach((o) => {
|
||||
const id = String(o.orderid);
|
||||
const s = String(o.orderstatus || '').toLowerCase();
|
||||
if (s === 'active') {
|
||||
stickyActiveRef.current[id] = o;
|
||||
} else if (TERMINAL.has(s)) {
|
||||
delete stickyActiveRef.current[id];
|
||||
}
|
||||
// Non-terminal, non-active statuses for a known order are intentionally
|
||||
// ignored — a brief status regression in the poll data must not evict a card.
|
||||
});
|
||||
|
||||
const stableOrders = Object.values(stickyActiveRef.current);
|
||||
if (!stableOrders.length) return [];
|
||||
|
||||
const riderMeta = Object.fromEntries(riders.map((r) => [r.id, r]));
|
||||
const byRider = {};
|
||||
stableOrders.forEach((o) => {
|
||||
const key = String(o.rider_id || o.userid || '');
|
||||
if (!key || key === 'unassigned' || key === '0') return;
|
||||
if (!byRider[key]) {
|
||||
const meta = riderMeta[key];
|
||||
byRider[key] = {
|
||||
id: key,
|
||||
riderName: meta?.riderName || o.rider_name || o.ridername || o.username || `Rider ${key}`,
|
||||
orders: [],
|
||||
color: meta?.color || getStableRiderColor(key)
|
||||
};
|
||||
}
|
||||
if (!byRider[key].orders.some((ex) => String(ex.orderid) === String(o.orderid))) {
|
||||
byRider[key].orders.push(o);
|
||||
}
|
||||
});
|
||||
return Object.values(byRider);
|
||||
}, [isAllActiveView, liveRows, riders]);
|
||||
|
||||
// Live GPS coordinates of exactly the riders this view shows (active + has an
|
||||
// order), fed to MapController's auto-fit so the map frames what's rendered.
|
||||
const allViewLivePoints = useMemo(
|
||||
@@ -1969,13 +2049,18 @@ const Dispatch = ({
|
||||
[isAllActiveView, liveRiderLocations, activeOrderRiderIdSet]
|
||||
);
|
||||
|
||||
// Count of in-progress deliveries shown in the Active view list. Drives the
|
||||
// sidebar header visibility — when the active fleet has nothing in progress,
|
||||
// the header (RIDER DISPATCH title + Active Fleet badge + order/rider tiles)
|
||||
// is hidden so the "No active deliveries" empty state stands on its own.
|
||||
// Clear the sticky active-order cache whenever the operator switches to a
|
||||
// different date or zone — stale cards from the previous filter must not leak
|
||||
// into the new view.
|
||||
useEffect(() => {
|
||||
stickyActiveRef.current = {};
|
||||
}, [selectedDate, selectedAppLocationId]);
|
||||
|
||||
// Count of in-progress deliveries shown in the Active view list. Derives from
|
||||
// the sticky cache (via activeViewRiders) so header and card list always agree.
|
||||
const activeDeliveryCount = useMemo(
|
||||
() => (isAllActiveView ? allViewOrders.filter(isActiveDelivery).length : 0),
|
||||
[isAllActiveView, allViewOrders]
|
||||
() => (isAllActiveView ? activeViewRiders.reduce((sum, r) => sum + r.orders.length, 0) : 0),
|
||||
[isAllActiveView, activeViewRiders]
|
||||
);
|
||||
|
||||
// Per-rider canvas renderer for the actual (right) map in Compare mode.
|
||||
@@ -2790,7 +2875,7 @@ const Dispatch = ({
|
||||
html: `<div class="kitchen-mark${focused ? ' is-focused' : ''}">${(name || 'K').charAt(0).toUpperCase()}</div>`
|
||||
});
|
||||
|
||||
const getRiderColor = (rid) => riders.find(r => r.id === rid)?.color || '#475569';
|
||||
const getRiderColor = (rid) => riders.find(r => String(r.id) === String(rid))?.color || '#475569';
|
||||
|
||||
const calculateEstMeters = (riderId, order) => {
|
||||
if (!riderId || !order || !hasValidDrop(order)) return null;
|
||||
@@ -3681,7 +3766,9 @@ const Dispatch = ({
|
||||
onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
|
||||
><span className="sbt-icon"><MdMap /></span> By Zone</button>
|
||||
<button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Rider'); setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
|
||||
{!embedded && (
|
||||
<button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Active Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span>Active</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`sbt sbt-profitability ${viewMode === 'profitability' ? 'active' : ''}`}
|
||||
@@ -4929,7 +5016,7 @@ const Dispatch = ({
|
||||
))
|
||||
) : isAllActiveView ? (
|
||||
<ActiveSection
|
||||
visibleRiders={visibleRiders}
|
||||
visibleRiders={activeViewRiders}
|
||||
riders={riders}
|
||||
focusedStop={focusedStop}
|
||||
handleRiderFocus={handleRiderFocus}
|
||||
@@ -4937,6 +5024,7 @@ const Dispatch = ({
|
||||
calculateEstMeters={calculateEstMeters}
|
||||
getRiderColor={getRiderColor}
|
||||
formatMeters={formatMeters}
|
||||
isLoading={shouldFetchLive && liveIsFetching && activeViewRiders.length === 0}
|
||||
/>
|
||||
) : (
|
||||
visibleRiders.map(renderRiderCard)
|
||||
|
||||
Reference in New Issue
Block a user