updates on the dispatch page and active section

This commit is contained in:
2026-06-09 15:56:06 +05:30
parent fd27ac92d8
commit be0ff70ee4
3 changed files with 366 additions and 25 deletions

View File

@@ -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;

View File

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

View File

@@ -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.