updates on the dispatch page and active section
This commit is contained in:
@@ -2113,6 +2113,194 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Active-delivery card (Active view sidebar) ─────────────────────────────
|
||||||
|
Clean monitoring card: rider avatar + customer/rider header + status pill, a
|
||||||
|
truncated drop-address line, and a footer with the pickup kitchen on the left
|
||||||
|
and distance + ETA-to-drop on the right. `--ad-accent` = owning rider color. */
|
||||||
|
.dispatch-container .adcard-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 4px 2px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard {
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 13px 14px 13px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--ad-accent, var(--accent));
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: width 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
|
||||||
|
border-color: var(--ad-accent, var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard:hover::before {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard.is-active {
|
||||||
|
border-color: var(--ad-accent, var(--accent));
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard.is-active::before {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-titles {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-customer {
|
||||||
|
font-size: 14.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-rider {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 4px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-addr {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: 11px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-addr .adcard-ic {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 11px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-pickup {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-pickup .adcard-ic {
|
||||||
|
color: var(--kitchen);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-metrics {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-m {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-m-km {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-m-eta {
|
||||||
|
color: var(--ad-accent, var(--accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-ic {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dispatch-container .adcard-tx {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.dispatch-container .zone-order-change-rider {
|
.dispatch-container .zone-order-change-rider {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ import {
|
|||||||
getStatusStyle,
|
getStatusStyle,
|
||||||
FINAL_STATUSES,
|
FINAL_STATUSES,
|
||||||
SKIPPED_STATUSES,
|
SKIPPED_STATUSES,
|
||||||
|
isActiveDelivery,
|
||||||
|
getActiveOrder,
|
||||||
STEP_PALETTE,
|
STEP_PALETTE,
|
||||||
stepColor
|
stepColor
|
||||||
} from './dispatchShared';
|
} from './dispatchShared';
|
||||||
@@ -1706,6 +1708,15 @@ const Dispatch = ({
|
|||||||
};
|
};
|
||||||
}, [focusedRider, focusedKitchen, isAllActiveView, allViewOrders, visibleRiders, stats]);
|
}, [focusedRider, focusedKitchen, isAllActiveView, allViewOrders, visibleRiders, stats]);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const activeDeliveryCount = useMemo(
|
||||||
|
() => (isAllActiveView ? allViewOrders.filter(isActiveDelivery).length : 0),
|
||||||
|
[isAllActiveView, allViewOrders]
|
||||||
|
);
|
||||||
|
|
||||||
// List of deliveryids we want GPS logs for. Drives two pipelines:
|
// List of deliveryids we want GPS logs for. Drives two pipelines:
|
||||||
// • renderRoutes() — actual-route polylines on the main map for
|
// • renderRoutes() — actual-route polylines on the main map for
|
||||||
// every visible rider/trip
|
// every visible rider/trip
|
||||||
@@ -2499,6 +2510,58 @@ const Dispatch = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Delivery-centric card for the "Active" view sidebar. Instead of one card
|
||||||
|
// per rider, the Active view lists the in-progress deliveries themselves so
|
||||||
|
// 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 || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={o.orderid}
|
||||||
|
className="rcard active-delivery-card"
|
||||||
|
onClick={() => {
|
||||||
|
if (rider) handleRiderFocus(rider);
|
||||||
|
if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
|
||||||
|
}}
|
||||||
|
style={{ animationDelay: `${i * 0.05}s` }}
|
||||||
|
>
|
||||||
|
<div className="rcard-top">
|
||||||
|
<div className="rcard-emo" style={{ background: `${color}18`, borderColor: `${color}50`, color }}><MdLocationOn /></div>
|
||||||
|
<div className="rcard-info">
|
||||||
|
<div className="rcard-name" title={customer}>{customer}</div>
|
||||||
|
<div className="rcard-zone">
|
||||||
|
<Ico><MdTwoWheeler /></Ico>{o.rider_name || o.ridername || 'Unassigned'}{dropArea ? ` · ${dropArea}` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rcard-badge" style={{ background: statusStyle.bg, color: statusStyle.fg }} title={statusStyle.label}>
|
||||||
|
{statusStyle.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rcard-meta">
|
||||||
|
{o.pickupcustomer && <span><Ico><MdRestaurant /></Ico>{o.pickupcustomer}</span>}
|
||||||
|
<span><Ico><MdStraighten /></Ico>{parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km</span>
|
||||||
|
{estMeters !== null && (
|
||||||
|
<span className="rcard-est-meters" title="Estimated distance to drop location">
|
||||||
|
<Ico><MdMyLocation /></Ico>{formatMeters(estMeters)} to drop
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Returns true when the order's centered popup should stay open even after
|
// 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
|
// 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
|
// matching compare-step is focused (so clicking a step in the right panel
|
||||||
@@ -2666,6 +2729,21 @@ const Dispatch = ({
|
|||||||
if (focusedRider) ordersToRender = focusedRider.orders;
|
if (focusedRider) ordersToRender = focusedRider.orders;
|
||||||
ordersToRender = ordersToRender.filter(hasValidDrop);
|
ordersToRender = ordersToRender.filter(hasValidDrop);
|
||||||
|
|
||||||
|
// Active view drop pins:
|
||||||
|
// • Focused rider → only that rider's single in-progress delivery, so the
|
||||||
|
// pin lines up with the single active polyline drawn in renderRoutes().
|
||||||
|
// • Overview (no focus) → every active delivery, matching the active-
|
||||||
|
// delivery list in the sidebar.
|
||||||
|
if (isAllActiveView) {
|
||||||
|
if (focusedRider) {
|
||||||
|
const active = getActiveOrder(focusedRider.orders);
|
||||||
|
const id = active ? String(active.orderid) : null;
|
||||||
|
ordersToRender = id ? ordersToRender.filter((o) => String(o.orderid) === id) : [];
|
||||||
|
} else {
|
||||||
|
ordersToRender = ordersToRender.filter(isActiveDelivery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-build the deliveryid → sequenceStep lookup once per render so each
|
// Pre-build the deliveryid → sequenceStep lookup once per render so each
|
||||||
// marker can resolve its step palette color without an O(N) scan.
|
// marker can resolve its step palette color without an O(N) scan.
|
||||||
const compareDeliveryToStep =
|
const compareDeliveryToStep =
|
||||||
@@ -2787,7 +2865,12 @@ const Dispatch = ({
|
|||||||
if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
|
if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
|
||||||
if (zoneRiderIds && !zoneRiderIds.has(String(r.id))) return;
|
if (zoneRiderIds && !zoneRiderIds.has(String(r.id))) return;
|
||||||
|
|
||||||
const rOrders = r.orders;
|
// In the Active view, collapse the rider down to just their in-progress
|
||||||
|
// delivery so the map draws only that one leg's polyline instead of the
|
||||||
|
// whole day's route. Every other view keeps the full order list.
|
||||||
|
const activeOrder = isAllActiveView ? getActiveOrder(r.orders) : null;
|
||||||
|
const rOrders = isAllActiveView ? (activeOrder ? [activeOrder] : []) : r.orders;
|
||||||
|
if (isAllActiveView && rOrders.length === 0) return;
|
||||||
const trips = {};
|
const trips = {};
|
||||||
rOrders.forEach(o => {
|
rOrders.forEach(o => {
|
||||||
const t = o.trip_number || 1;
|
const t = o.trip_number || 1;
|
||||||
@@ -2882,8 +2965,12 @@ const Dispatch = ({
|
|||||||
// false → OSRM permanently failed (draw aerial fallback so user sees something)
|
// false → OSRM permanently failed (draw aerial fallback so user sees something)
|
||||||
// null → request in-flight (DON'T draw anything yet — avoids the aerial flash)
|
// null → request in-flight (DON'T draw anything yet — avoids the aerial flash)
|
||||||
// undefined → not yet requested (same as in-flight, wait)
|
// undefined → not yet requested (same as in-flight, wait)
|
||||||
const hasRoad = Array.isArray(roadPoints) && roadPoints.length >= 2;
|
// The cached OSRM route is keyed per *trip*, so it covers every stop
|
||||||
const failed = roadPoints === false;
|
// and can't represent a single active leg. In the Active view we skip
|
||||||
|
// it and build the planned line straight from the (already single-order)
|
||||||
|
// `sorted` list, so the fallback stays scoped to the active delivery.
|
||||||
|
const hasRoad = !isAllActiveView && Array.isArray(roadPoints) && roadPoints.length >= 2;
|
||||||
|
const failed = isAllActiveView ? true : roadPoints === false;
|
||||||
if (!hasRoad && !failed) return; // still loading — don't show aerial flash
|
if (!hasRoad && !failed) return; // still loading — don't show aerial flash
|
||||||
|
|
||||||
const finalPoints = hasRoad ? roadPoints : buildTripPoints(sorted);
|
const finalPoints = hasRoad ? roadPoints : buildTripPoints(sorted);
|
||||||
@@ -3738,9 +3825,10 @@ const Dispatch = ({
|
|||||||
)}
|
)}
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
{/* Sidebar header — replaces the top-bar meta line. Hidden when a specific
|
{/* Sidebar header — replaces the top-bar meta line. Hidden when a specific
|
||||||
rider is focused, since the focused-rider view already shows that rider's
|
rider is focused (the focused-rider view shows that rider's stats), and
|
||||||
stats prominently (name + Orders/Distance tiles). */}
|
also hidden in the Active view when there are no in-progress deliveries,
|
||||||
{!focusedRider && (
|
so the empty state isn't topped by a "0 orders / 0 riders" header. */}
|
||||||
|
{!focusedRider && !(isAllActiveView && activeDeliveryCount === 0) && (
|
||||||
<div className="sb-header">
|
<div className="sb-header">
|
||||||
<div className="sb-header-top">
|
<div className="sb-header-top">
|
||||||
<div className="sb-header-title">
|
<div className="sb-header-title">
|
||||||
@@ -3787,12 +3875,16 @@ const Dispatch = ({
|
|||||||
<>
|
<>
|
||||||
<div className="rd-rider-name" style={{ color: focusedRider.color }}>{focusedRider.riderName}</div>
|
<div className="rd-rider-name" style={{ color: focusedRider.color }}>{focusedRider.riderName}</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const totalKm = focusedRider.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
|
// Active view: the panel monitors just the in-progress
|
||||||
|
// delivery, so the tiles reflect that one stop alone.
|
||||||
|
const activeOnly = isAllActiveView ? getActiveOrder(focusedRider.orders) : null;
|
||||||
|
const panelOrders = isAllActiveView ? (activeOnly ? [activeOnly] : []) : focusedRider.orders;
|
||||||
|
const totalKm = panelOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
|
||||||
return (
|
return (
|
||||||
<div className="rd-stats-grid">
|
<div className="rd-stats-grid">
|
||||||
<div className="rd-stat rd-stat-orders">
|
<div className="rd-stat rd-stat-orders">
|
||||||
<div className="rd-stat-icon"><MdInventory2 /></div>
|
<div className="rd-stat-icon"><MdInventory2 /></div>
|
||||||
<div className="rd-stat-value">{focusedRider.orders.length}</div>
|
<div className="rd-stat-value">{panelOrders.length}</div>
|
||||||
<div className="rd-stat-label">Orders</div>
|
<div className="rd-stat-label">Orders</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rd-stat rd-stat-distance">
|
<div className="rd-stat rd-stat-distance">
|
||||||
@@ -3804,8 +3896,20 @@ const Dispatch = ({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{(() => {
|
{(() => {
|
||||||
|
// Active view: collapse the trip list down to the single
|
||||||
|
// in-progress delivery so the panel shows the active
|
||||||
|
// delivery alone (matches the single polyline on the map).
|
||||||
|
const activeOnly = isAllActiveView ? getActiveOrder(focusedRider.orders) : null;
|
||||||
|
const panelOrders = isAllActiveView ? (activeOnly ? [activeOnly] : []) : focusedRider.orders;
|
||||||
|
if (isAllActiveView && panelOrders.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="trip-empty-note" style={{ padding: '14px 4px', color: '#94a3b8', fontSize: 13 }}>
|
||||||
|
No active delivery right now.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
const trips = {};
|
const trips = {};
|
||||||
focusedRider.orders.forEach(o => {
|
panelOrders.forEach(o => {
|
||||||
const t = o.trip_number || 1;
|
const t = o.trip_number || 1;
|
||||||
if (!trips[t]) trips[t] = [];
|
if (!trips[t]) trips[t] = [];
|
||||||
trips[t].push(o);
|
trips[t].push(o);
|
||||||
@@ -4300,12 +4404,17 @@ const Dispatch = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div id="riders-panel">
|
<div id="riders-panel">
|
||||||
<div className="ph">{
|
{/* Hide the panel title in the Active view when there are no
|
||||||
viewMode === 'zones' ? 'Zone dispatch' :
|
in-progress deliveries, so the empty state isn't topped by
|
||||||
viewMode === 'kitchens' ? 'Kitchen dispatch' :
|
an "Active rider dispatch" heading. */}
|
||||||
viewMode === 'all' ? 'Active rider dispatch' :
|
{!(isAllActiveView && activeDeliveryCount === 0) && (
|
||||||
'Rider dispatch'
|
<div className="ph">{
|
||||||
}</div>
|
viewMode === 'zones' ? 'Zone dispatch' :
|
||||||
|
viewMode === 'kitchens' ? 'Kitchen dispatch' :
|
||||||
|
viewMode === 'all' ? 'Active rider dispatch' :
|
||||||
|
'Rider dispatch'
|
||||||
|
}</div>
|
||||||
|
)}
|
||||||
<div id="rider-cards">
|
<div id="rider-cards">
|
||||||
{allOrders.length === 0 && !liveIsFetching ? (
|
{allOrders.length === 0 && !liveIsFetching ? (
|
||||||
(() => {
|
(() => {
|
||||||
@@ -4422,16 +4531,38 @@ const Dispatch = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : isAllActiveView && visibleRiders.length === 0 ? (
|
) : isAllActiveView ? (
|
||||||
<div className="empty-slot">
|
// Active view: list the in-progress deliveries themselves,
|
||||||
<div className="empty-slot-icon">
|
// grouped by rider name then trip/step — NOT rider cards.
|
||||||
<MdTwoWheeler />
|
(() => {
|
||||||
</div>
|
const activeDeliveries = allViewOrders
|
||||||
<div className="empty-slot-title">No active riders</div>
|
.filter(isActiveDelivery)
|
||||||
<div className="empty-slot-sub">
|
.slice()
|
||||||
No riders are currently live on the road for this slot
|
.sort((a, b) => {
|
||||||
</div>
|
const rn = String(a.rider_name || a.ridername || '').localeCompare(
|
||||||
</div>
|
String(b.rider_name || b.ridername || '')
|
||||||
|
);
|
||||||
|
if (rn !== 0) return rn;
|
||||||
|
const tA = a.trip_number || 1;
|
||||||
|
const tB = b.trip_number || 1;
|
||||||
|
if (tA !== tB) return tA - tB;
|
||||||
|
return (a.step || 0) - (b.step || 0);
|
||||||
|
});
|
||||||
|
if (activeDeliveries.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="empty-slot">
|
||||||
|
<div className="empty-slot-icon">
|
||||||
|
<MdInventory2 />
|
||||||
|
</div>
|
||||||
|
<div className="empty-slot-title">No active deliveries</div>
|
||||||
|
<div className="empty-slot-sub">
|
||||||
|
No deliveries are currently in progress for this slot
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return activeDeliveries.map(renderActiveDeliveryCard);
|
||||||
|
})()
|
||||||
) : (
|
) : (
|
||||||
visibleRiders.map(renderRiderCard)
|
visibleRiders.map(renderRiderCard)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -29,6 +29,28 @@ export const getStatusStyle = (status) =>
|
|||||||
export const FINAL_STATUSES = new Set(['delivered']);
|
export const FINAL_STATUSES = new Set(['delivered']);
|
||||||
export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
|
export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
|
||||||
|
|
||||||
|
// An order is "active" (currently in progress) when it's neither completed
|
||||||
|
// (delivered) nor skipped/cancelled. The Active view uses this to collapse a
|
||||||
|
// rider down to the single delivery they're working on right now.
|
||||||
|
export const isActiveDelivery = (o) => {
|
||||||
|
const s = String(o?.orderstatus || '').toLowerCase();
|
||||||
|
return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A rider's single in-progress delivery: the first non-final, non-skipped
|
||||||
|
// stop in (trip, step) order. Returns null when the rider has nothing active
|
||||||
|
// (everything delivered/cancelled, or GPS-only with no orders).
|
||||||
|
export const getActiveOrder = (orders) => {
|
||||||
|
if (!Array.isArray(orders) || !orders.length) return null;
|
||||||
|
const sorted = [...orders].sort((a, b) => {
|
||||||
|
const tA = a.trip_number || 1;
|
||||||
|
const tB = b.trip_number || 1;
|
||||||
|
if (tA !== tB) return tA - tB;
|
||||||
|
return (a.step || 0) - (b.step || 0);
|
||||||
|
});
|
||||||
|
return sorted.find(isActiveDelivery) || null;
|
||||||
|
};
|
||||||
|
|
||||||
// Per-step palette — wider and more deliberately spaced than the rider
|
// Per-step palette — wider and more deliberately spaced than the rider
|
||||||
// palette so a 10-stop day reads as 10 distinct colors on the compare
|
// palette so a 10-stop day reads as 10 distinct colors on the compare
|
||||||
// map's polylines + pins.
|
// map's polylines + pins.
|
||||||
|
|||||||
Reference in New Issue
Block a user