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

@@ -56,6 +56,8 @@ import {
getStatusStyle,
FINAL_STATUSES,
SKIPPED_STATUSES,
isActiveDelivery,
getActiveOrder,
STEP_PALETTE,
stepColor
} from './dispatchShared';
@@ -1706,6 +1708,15 @@ const Dispatch = ({
};
}, [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:
// • renderRoutes() — actual-route polylines on the main map for
// 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
// 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
@@ -2666,6 +2729,21 @@ const Dispatch = ({
if (focusedRider) ordersToRender = focusedRider.orders;
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
// marker can resolve its step palette color without an O(N) scan.
const compareDeliveryToStep =
@@ -2787,7 +2865,12 @@ const Dispatch = ({
if (focusedKitchen && !focusedKitchen.riders.has(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 = {};
rOrders.forEach(o => {
const t = o.trip_number || 1;
@@ -2882,8 +2965,12 @@ const Dispatch = ({
// false → OSRM permanently failed (draw aerial fallback so user sees something)
// null → request in-flight (DON'T draw anything yet — avoids the aerial flash)
// undefined → not yet requested (same as in-flight, wait)
const hasRoad = Array.isArray(roadPoints) && roadPoints.length >= 2;
const failed = roadPoints === false;
// The cached OSRM route is keyed per *trip*, so it covers every stop
// 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
const finalPoints = hasRoad ? roadPoints : buildTripPoints(sorted);
@@ -3738,9 +3825,10 @@ const Dispatch = ({
)}
<div id="sidebar">
{/* 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
stats prominently (name + Orders/Distance tiles). */}
{!focusedRider && (
rider is focused (the focused-rider view shows that rider's stats), and
also hidden in the Active view when there are no in-progress deliveries,
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-top">
<div className="sb-header-title">
@@ -3787,12 +3875,16 @@ const Dispatch = ({
<>
<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 (
<div className="rd-stats-grid">
<div className="rd-stat rd-stat-orders">
<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>
<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 = {};
focusedRider.orders.forEach(o => {
panelOrders.forEach(o => {
const t = o.trip_number || 1;
if (!trips[t]) trips[t] = [];
trips[t].push(o);
@@ -4300,12 +4404,17 @@ const Dispatch = ({
</div>
) : (
<div id="riders-panel">
<div className="ph">{
viewMode === 'zones' ? 'Zone dispatch' :
viewMode === 'kitchens' ? 'Kitchen dispatch' :
viewMode === 'all' ? 'Active rider dispatch' :
'Rider dispatch'
}</div>
{/* Hide the panel title in the Active view when there are no
in-progress deliveries, so the empty state isn't topped by
an "Active rider dispatch" heading. */}
{!(isAllActiveView && activeDeliveryCount === 0) && (
<div className="ph">{
viewMode === 'zones' ? 'Zone dispatch' :
viewMode === 'kitchens' ? 'Kitchen dispatch' :
viewMode === 'all' ? 'Active rider dispatch' :
'Rider dispatch'
}</div>
)}
<div id="rider-cards">
{allOrders.length === 0 && !liveIsFetching ? (
(() => {
@@ -4422,16 +4531,38 @@ const Dispatch = ({
</div>
</div>
))
) : isAllActiveView && visibleRiders.length === 0 ? (
<div className="empty-slot">
<div className="empty-slot-icon">
<MdTwoWheeler />
</div>
<div className="empty-slot-title">No active riders</div>
<div className="empty-slot-sub">
No riders are currently live on the road for this slot
</div>
</div>
) : isAllActiveView ? (
// Active view: list the in-progress deliveries themselves,
// grouped by rider name then trip/step — NOT rider cards.
(() => {
const activeDeliveries = allViewOrders
.filter(isActiveDelivery)
.slice()
.sort((a, b) => {
const rn = String(a.rider_name || a.ridername || '').localeCompare(
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)
)}