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);
}
/* ── 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 {
flex-shrink: 0;
width: 26px;

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">
{/* 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 ? (
) : 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">
<MdTwoWheeler />
<MdInventory2 />
</div>
<div className="empty-slot-title">No active riders</div>
<div className="empty-slot-title">No active deliveries</div>
<div className="empty-slot-sub">
No riders are currently live on the road for this slot
No deliveries are currently in progress for this slot
</div>
</div>
);
}
return activeDeliveries.map(renderActiveDeliveryCard);
})()
) : (
visibleRiders.map(renderRiderCard)
)}

View File

@@ -29,6 +29,28 @@ export const getStatusStyle = (status) =>
export const FINAL_STATUSES = new Set(['delivered']);
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
// palette so a 10-stop day reads as 10 distinct colors on the compare
// map's polylines + pins.