updates on the dispatch page and the css

This commit is contained in:
2026-06-09 18:08:11 +05:30
parent be0ff70ee4
commit b6f5cdcaa8
2 changed files with 222 additions and 101 deletions

View File

@@ -2203,13 +2203,14 @@
} }
.dispatch-container .adcard-rider { .dispatch-container .adcard-rider {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
color: var(--text-muted); color: var(--text-muted);
margin-top: 1px; margin-top: 2px;
white-space: nowrap; min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
} }
.dispatch-container .adcard-status { .dispatch-container .adcard-status {
@@ -2300,6 +2301,25 @@
min-width: 0; min-width: 0;
} }
/* Drop address: allow up to two lines instead of one truncated line so long
addresses stay readable; the location icon hugs the first line. */
.dispatch-container .adcard-addr {
align-items: flex-start;
}
.dispatch-container .adcard-addr .adcard-ic {
margin-top: 1px;
}
.dispatch-container .adcard-addr-tx {
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.dispatch-container .zone-order-change-rider { .dispatch-container .zone-order-change-rider {
flex-shrink: 0; flex-shrink: 0;

View File

@@ -1298,7 +1298,14 @@ const Dispatch = ({
queryKey: ['dispatchDeliveries', selectedAppLocationId, liveUserid, 'all', selectedDate, selectedDate, 50, '', 0, 0, 0], queryKey: ['dispatchDeliveries', selectedAppLocationId, liveUserid, 'all', selectedDate, selectedDate, 50, '', 0, 0, 0],
queryFn: fetchDeliveries, queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
enabled: shouldFetchLive enabled: shouldFetchLive,
// Order status (pending → delivered) only lives in this feed. Poll it in the
// Active view so a completed delivery drops out and the rider's NEXT active
// leg automatically becomes the one shown (card + route + flag) without a
// manual refresh. Other views don't need second-by-second order churn, so
// they refetch only on the usual triggers (date/slot/hub change, refocus).
refetchInterval: viewMode === 'all' ? 15_000 : false,
refetchIntervalInBackground: false
}); });
// Auto-page through all results for the selected date. // Auto-page through all results for the selected date.
@@ -1554,58 +1561,52 @@ const Dispatch = ({
? (selectedRiderId ? (riders.find((r) => r.id === selectedRiderId) || null) : null) ? (selectedRiderId ? (riders.find((r) => r.id === selectedRiderId) || null) : null)
: internalFocusedRider; : internalFocusedRider;
// "All Active Routes" view scoping. In this mode we render only riders whose // "All Active Routes" view scoping. This view is ORDER-CENTRIC: it shows ONLY
// live GPS log is `active` right now — their cards, routes, drop markers and // riders who are live on GPS right now AND still have an in-progress order —
// bike markers — so the operator sees the on-road fleet at a glance. Every // their card in the sidebar, their single active-leg route line, and their
// other view (By Location / By Zone / By Rider) is unchanged. // live bike marker. Riders who are on GPS but have nothing left to deliver
// (everything delivered, or GPS-only with no orders) are intentionally
// excluded here. Every other view (By Location / By Zone / By Rider) is
// unchanged.
const isAllActiveView = viewMode === 'all'; const isAllActiveView = viewMode === 'all';
// Active riders the GPS feed reports as on-road RIGHT NOW that have no orders // Orders belonging to GPS-active riders the candidate pool for this view's
// in the current data (i.e. nothing to deliver). We still want them on screen // list, drop set and auto-fit. Narrowed to in-progress orders below.
// in "All Active Routes" — just their live bike marker, no route — so we
// synthesize order-less rider objects shaped like real ones. `gpsOnly` flags
// them so the card / route code treats them as "live position only".
const gpsOnlyActiveRiders = useMemo(() => {
if (!isAllActiveView) return [];
const haveOrders = new Set(riders.map((r) => String(r.id)));
return liveRiderLocations
.filter((r) => r.status === 'active' && !haveOrders.has(String(r.id)))
.map((r) => ({
id: r.id,
riderName: r.username || `Rider #${r.id}`,
orders: [],
color: getStableRiderColor(r.id),
gpsOnly: true,
// Live position — lets MapController center on the rider when their
// GPS-only card is clicked (they have no drops to fit to).
lat: r.lat,
lon: r.lon
}));
}, [isAllActiveView, liveRiderLocations, riders]);
// The riders we render in "All Active Routes": every rider whose live GPS is
// active — those with orders (real route shown) PLUS those without (GPS only).
// Every other view (By Location / By Zone / By Rider) is unchanged.
const visibleRiders = useMemo(
() =>
isAllActiveView
? [...riders.filter((r) => activeRiderIdSet.has(String(r.id))), ...gpsOnlyActiveRiders]
: riders,
[isAllActiveView, riders, activeRiderIdSet, gpsOnlyActiveRiders]
);
// Orders that belong to the riders we're actually showing — drives the drop
// markers and the map auto-fit bounds in the active view.
const allViewOrders = useMemo( const allViewOrders = useMemo(
() => (isAllActiveView ? allOrders.filter((o) => activeRiderIdSet.has(String(o.rider_id))) : allOrders), () => (isAllActiveView ? allOrders.filter((o) => activeRiderIdSet.has(String(o.rider_id))) : allOrders),
[isAllActiveView, allOrders, activeRiderIdSet] [isAllActiveView, allOrders, activeRiderIdSet]
); );
// Live GPS coordinates of every active rider in the "All Active Routes" view. // The single gate for the whole Active view: rider ids that are GPS-active AND
// Fed to MapController's auto-fit so order-less (GPS-only) active riders are // currently have an in-progress order. Driving the sidebar list, routes,
// framed even when there are no drop markers to anchor the bounds. // markers and map-fit off this one set guarantees they can never disagree
// about which riders are shown.
const activeOrderRiderIdSet = useMemo(
() =>
new Set(
(isAllActiveView ? allViewOrders : [])
.filter(isActiveDelivery)
.map((o) => String(o.rider_id))
),
[isAllActiveView, allViewOrders]
);
// The riders we render in "All Active Routes": GPS-active riders that still
// have an active order. Every other view is unchanged.
const visibleRiders = useMemo(
() =>
isAllActiveView
? riders.filter((r) => activeOrderRiderIdSet.has(String(r.id)))
: riders,
[isAllActiveView, riders, activeOrderRiderIdSet]
);
// 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( const allViewLivePoints = useMemo(
() => () =>
isAllActiveView isAllActiveView
? liveRiderLocations.filter((r) => r.status === 'active').map((r) => [r.lat, r.lon]) ? liveRiderLocations
.filter((r) => r.status === 'active' && activeOrderRiderIdSet.has(String(r.id)))
.map((r) => [r.lat, r.lon])
: [], : [],
[isAllActiveView, liveRiderLocations] [isAllActiveView, liveRiderLocations, activeOrderRiderIdSet]
); );
// Per-rider canvas renderer for the actual (right) map in Compare mode. // Per-rider canvas renderer for the actual (right) map in Compare mode.
@@ -1687,15 +1688,17 @@ const Dispatch = ({
}; };
} }
// "All Active Routes": the header must reflect exactly what the list/map // "All Active Routes": the header must reflect exactly what the list/map
// shows — the active fleet (riders with orders + GPS-only riders) and their // shows — the in-progress orders and the riders working them — NOT the whole
// orders — NOT the whole day's totals. Otherwise the top "Riders" tile (full // day's totals. We count only active deliveries (and `visibleRiders`, which
// fleet) disagrees with the rider list below (active-only). // is already gated to active-order riders) so the tiles can't disagree with
// the list/map below.
if (isAllActiveView) { if (isAllActiveView) {
const activeOrders = allViewOrders.filter(isActiveDelivery);
return { return {
orders: allViewOrders.length, orders: activeOrders.length,
riders: visibleRiders.length, riders: visibleRiders.length,
km: allViewOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0), km: activeOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
profit: allViewOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0), profit: activeOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
label: 'Active Fleet' label: 'Active Fleet'
}; };
} }
@@ -2183,8 +2186,30 @@ const Dispatch = ({
const pts = buildTripPoints(sorted); const pts = buildTripPoints(sorted);
if (pts.length >= 2) fetchRoute(r.id, tNum, pts); if (pts.length >= 2) fetchRoute(r.id, tNum, pts);
}); });
// Active view: fetch a road-following route scoped to the rider's single
// in-progress leg — current live GPS position → drop (pickup as fallback
// when no live fix). Cached under a per-order key so renderRoutes() can
// draw the road polyline for that leg instead of a straight line, and so
// it never collides with the per-trip route fetched above.
if (isAllActiveView) {
const activeOrder = getActiveOrder(r.orders);
if (activeOrder) {
const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
? [lp.lat, lp.lon]
: (hasValidPickup(activeOrder)
? [toNum(pickupLat(activeOrder)), toNum(pickupLon(activeOrder))]
: null);
const dLat = toNum(activeOrder.droplat || activeOrder.deliverylat);
const dLon = toNum(activeOrder.droplon || activeOrder.deliverylong);
if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
fetchRoute(r.id, `active-${activeOrder.orderid}`, [start, [dLat, dLon]]);
}
}
}
}); });
}, [riders, activeRiders, focusedRider, fetchRoute]); }, [riders, activeRiders, focusedRider, isAllActiveView, liveRiderLocations, fetchRoute]);
// Auto-advance the selected slot when the wall-clock moves into a new slot's // Auto-advance the selected slot when the wall-clock moves into a new slot's
// window — BUT only if the user is still sitting on the slot that's just been // window — BUT only if the user is still sitting on the slot that's just been
@@ -2526,37 +2551,75 @@ const Dispatch = ({
const estMeters = calculateEstMeters(rid, o); const estMeters = calculateEstMeters(rid, o);
const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`; const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`;
const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || ''; const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || '';
const riderName = o.rider_name || o.ridername || 'Unassigned';
// This card is "active" (popped on the map) when its drop is the focused stop.
const isActive = canFocus && focusedStop && String(focusedStop.orderid) === String(o.orderid);
// Up-to-two-letter rider initials for the avatar.
const initials =
riderName
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0])
.join('')
.toUpperCase() || '•';
return ( return (
<div <div
key={o.orderid} key={o.orderid}
className="rcard active-delivery-card" className={`adcard${isActive ? ' is-active' : ''}`}
style={{ '--ad-accent': color, animationDelay: `${i * 0.05}s` }}
onClick={() => { onClick={() => {
if (rider) handleRiderFocus(rider); if (rider) handleRiderFocus(rider);
if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon }); if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
}} }}
style={{ animationDelay: `${i * 0.05}s` }}
> >
<div className="rcard-top"> <div className="adcard-top">
<div className="rcard-emo" style={{ background: `${color}18`, borderColor: `${color}50`, color }}><MdLocationOn /></div> <div className="adcard-avatar" style={{ background: color }}>{initials}</div>
<div className="rcard-info"> <div className="adcard-titles">
<div className="rcard-name" title={customer}>{customer}</div> <div className="adcard-customer" title={customer}>{customer}</div>
<div className="rcard-zone"> <div className="adcard-rider" title={riderName}>
<Ico><MdTwoWheeler /></Ico>{o.rider_name || o.ridername || 'Unassigned'}{dropArea ? ` · ${dropArea}` : ''} <MdTwoWheeler style={{ fontSize: 13, flexShrink: 0 }} />
<span className="adcard-tx">{riderName}</span>
</div> </div>
</div> </div>
<div className="rcard-badge" style={{ background: statusStyle.bg, color: statusStyle.fg }} title={statusStyle.label}> <span
className="adcard-status"
style={{ background: `${statusStyle.bg}1a`, color: statusStyle.bg }}
title={statusStyle.label}
>
{statusStyle.label} {statusStyle.label}
</div> </span>
</div> </div>
<div className="rcard-meta">
{o.pickupcustomer && <span><Ico><MdRestaurant /></Ico>{o.pickupcustomer}</span>} {dropArea && (
<span><Ico><MdStraighten /></Ico>{parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km</span> <div className="adcard-addr">
{estMeters !== null && ( <span className="adcard-ic"><MdLocationOn /></span>
<span className="rcard-est-meters" title="Estimated distance to drop location"> <span className="adcard-tx adcard-addr-tx" title={dropArea}>{dropArea}</span>
<Ico><MdMyLocation /></Ico>{formatMeters(estMeters)} to drop </div>
)}
<div className="adcard-foot">
{o.pickupcustomer ? (
<span className="adcard-pickup" title={o.pickupcustomer}>
<span className="adcard-ic"><MdRestaurant /></span>
<span className="adcard-tx">{o.pickupcustomer}</span>
</span> </span>
) : (
<span />
)} )}
<span className="adcard-metrics">
<span className="adcard-m adcard-m-km" title="Trip distance">
<span className="adcard-ic"><MdStraighten /></span>
{parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km
</span>
{estMeters !== null && (
<span className="adcard-m adcard-m-eta" title="Estimated distance to drop location">
<span className="adcard-ic"><MdMyLocation /></span>
{formatMeters(estMeters)}
</span>
)}
</span>
</div> </div>
</div> </div>
); );
@@ -2729,18 +2792,25 @@ const Dispatch = ({
if (focusedRider) ordersToRender = focusedRider.orders; if (focusedRider) ordersToRender = focusedRider.orders;
ordersToRender = ordersToRender.filter(hasValidDrop); ordersToRender = ordersToRender.filter(hasValidDrop);
// Active view drop pins: // Active view drop pins: show ONE flag per active rider — the drop of the
// • Focused rider → only that rider's single in-progress delivery, so the // leg they're currently heading to (getActiveOrder) — so the flag marks
// pin lines up with the single active polyline drawn in renderRoutes(). // "where the rider is going" and sits exactly at the end of that rider's
// • Overview (no focus) → every active delivery, matching the active- // route line. No other drops are flagged in this view.
// delivery list in the sidebar. // • Focused rider → just that rider's in-progress drop.
// • Overview (no focus) → each visible active rider's in-progress drop.
if (isAllActiveView) { if (isAllActiveView) {
if (focusedRider) { if (focusedRider) {
const active = getActiveOrder(focusedRider.orders); const active = getActiveOrder(focusedRider.orders);
const id = active ? String(active.orderid) : null; const id = active ? String(active.orderid) : null;
ordersToRender = id ? ordersToRender.filter((o) => String(o.orderid) === id) : []; ordersToRender = id ? ordersToRender.filter((o) => String(o.orderid) === id) : [];
} else { } else {
ordersToRender = ordersToRender.filter(isActiveDelivery); const destOrderIds = new Set(
visibleRiders
.map((r) => getActiveOrder(r.orders))
.filter(Boolean)
.map((o) => String(o.orderid))
);
ordersToRender = ordersToRender.filter((o) => destOrderIds.has(String(o.orderid)));
} }
} }
@@ -2965,15 +3035,48 @@ 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)
// The cached OSRM route is keyed per *trip*, so it covers every stop // The cached per-trip OSRM route (`roadPoints`) covers every stop, so it
// and can't represent a single active leg. In the Active view we skip // can't represent a single active leg. In the Active view we instead
// it and build the planned line straight from the (already single-order) // build a route scoped to just the in-progress leg and FOLLOW THE ROADS.
// `sorted` list, so the fallback stays scoped to the active delivery. //
const hasRoad = !isAllActiveView && Array.isArray(roadPoints) && roadPoints.length >= 2; // Leg endpoints: the rider's CURRENT live GPS position → the drop. The
const failed = isAllActiveView ? true : roadPoints === false; // live position is preferred because (a) active riders always have a
if (!hasRoad && !failed) return; // still loading — don't show aerial flash // live GPS fix in this view, so the leg always has 2 valid points even
// when the order carries no pickup coordinates — the previous cause of a
// completely missing line — and (b) it shows the rider's REMAINING route
// to the customer. Falls back to the pickup when there's no live fix.
const activeLegOrder = isAllActiveView ? sorted[0] : null;
let activeStraightLeg = null;
if (isAllActiveView && activeLegOrder) {
const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
? [lp.lat, lp.lon]
: (hasValidPickup(activeLegOrder)
? [toNum(pickupLat(activeLegOrder)), toNum(pickupLon(activeLegOrder))]
: null);
const dLat = toNum(activeLegOrder.droplat || activeLegOrder.deliverylat);
const dLon = toNum(activeLegOrder.droplon || activeLegOrder.deliverylong);
if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
activeStraightLeg = [start, [dLat, dLon]];
}
}
// Prefer the OSRM road-following route for the active leg (fetched as
// `${r.id}-active-${orderid}` in the route-fetch effect); other views
// keep the per-trip route.
const roadToUse = isAllActiveView
? (activeLegOrder ? osrmRoutes[`${r.id}-active-${activeLegOrder.orderid}`] : undefined)
: roadPoints;
const hasRoad = Array.isArray(roadToUse) && roadToUse.length >= 2;
const failed = roadToUse === false;
// Other views: wait for OSRM so we don't flash an aerial line before the
// road polyline lands. Active view: NEVER wait — draw the straight leg
// immediately and let it upgrade to the road-following route, so the
// line is never invisible while OSRM is in flight.
if (!isAllActiveView && !hasRoad && !failed) return;
const finalPoints = hasRoad ? roadPoints : buildTripPoints(sorted); const finalPoints = hasRoad
? roadToUse
: (isAllActiveView ? activeStraightLeg : buildTripPoints(sorted));
if (!finalPoints || finalPoints.length < 2) return; if (!finalPoints || finalPoints.length < 2) return;
// Aerial fallback (OSRM permanently failed) is rendered dashed so it visually // Aerial fallback (OSRM permanently failed) is rendered dashed so it visually
// reads as an estimate vs. an actual routed road polyline. // reads as an estimate vs. an actual routed road polyline.
@@ -4532,22 +4635,20 @@ const Dispatch = ({
</div> </div>
)) ))
) : isAllActiveView ? ( ) : isAllActiveView ? (
// Active view: list the in-progress deliveries themselves, // Active view: list exactly ONE card per active rider — the
// grouped by rider name then trip/step — NOT rider cards. // single in-progress leg (getActiveOrder) the map draws a
// route + destination flag for. Driving the list off the
// same `visibleRiders` set the map uses keeps the sidebar
// and the map in lock-step (same count, same deliveries).
(() => { (() => {
const activeDeliveries = allViewOrders const activeDeliveries = visibleRiders
.filter(isActiveDelivery) .map((r) => getActiveOrder(r.orders))
.slice() .filter(Boolean)
.sort((a, b) => { .sort((a, b) =>
const rn = String(a.rider_name || a.ridername || '').localeCompare( String(a.rider_name || a.ridername || '').localeCompare(
String(b.rider_name || b.ridername || '') 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) { if (activeDeliveries.length === 0) {
return ( return (
<div className="empty-slot"> <div className="empty-slot">
@@ -4561,7 +4662,7 @@ const Dispatch = ({
</div> </div>
); );
} }
return activeDeliveries.map(renderActiveDeliveryCard); return <div className="adcard-list">{activeDeliveries.map(renderActiveDeliveryCard)}</div>;
})() })()
) : ( ) : (
visibleRiders.map(renderRiderCard) visibleRiders.map(renderRiderCard)
@@ -4636,7 +4737,7 @@ const Dispatch = ({
{liveRiderLocations {liveRiderLocations
.filter((r) => .filter((r) =>
isAllActiveView isAllActiveView
? r.status === 'active' ? (r.status === 'active' && activeOrderRiderIdSet.has(String(r.id)))
: riders.some((rd) => String(rd.id) === String(r.id)) : riders.some((rd) => String(rd.id) === String(r.id))
) )
.filter((r) => !focusedRider || String(focusedRider.id) === String(r.id)) .filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))