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 {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
min-width: 0;
}
.dispatch-container .adcard-status {
@@ -2300,6 +2301,25 @@
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 {
flex-shrink: 0;

View File

@@ -1298,7 +1298,14 @@ const Dispatch = ({
queryKey: ['dispatchDeliveries', selectedAppLocationId, liveUserid, 'all', selectedDate, selectedDate, 50, '', 0, 0, 0],
queryFn: fetchDeliveries,
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.
@@ -1554,58 +1561,52 @@ const Dispatch = ({
? (selectedRiderId ? (riders.find((r) => r.id === selectedRiderId) || null) : null)
: internalFocusedRider;
// "All Active Routes" view scoping. In this mode we render only riders whose
// live GPS log is `active` right now — their cards, routes, drop markers and
// bike markers — so the operator sees the on-road fleet at a glance. Every
// other view (By Location / By Zone / By Rider) is unchanged.
// "All Active Routes" view scoping. This view is ORDER-CENTRIC: it shows ONLY
// riders who are live on GPS right now AND still have an in-progress order —
// their card in the sidebar, their single active-leg route line, and their
// 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';
// Active riders the GPS feed reports as on-road RIGHT NOW that have no orders
// in the current data (i.e. nothing to deliver). We still want them on screen
// 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.
// Orders belonging to GPS-active riders the candidate pool for this view's
// list, drop set and auto-fit. Narrowed to in-progress orders below.
const allViewOrders = useMemo(
() => (isAllActiveView ? allOrders.filter((o) => activeRiderIdSet.has(String(o.rider_id))) : allOrders),
[isAllActiveView, allOrders, activeRiderIdSet]
);
// Live GPS coordinates of every active rider in the "All Active Routes" view.
// Fed to MapController's auto-fit so order-less (GPS-only) active riders are
// framed even when there are no drop markers to anchor the bounds.
// The single gate for the whole Active view: rider ids that are GPS-active AND
// currently have an in-progress order. Driving the sidebar list, routes,
// 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(
() =>
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.
@@ -1687,15 +1688,17 @@ const Dispatch = ({
};
}
// "All Active Routes": the header must reflect exactly what the list/map
// shows — the active fleet (riders with orders + GPS-only riders) and their
// orders — NOT the whole day's totals. Otherwise the top "Riders" tile (full
// fleet) disagrees with the rider list below (active-only).
// shows — the in-progress orders and the riders working them — NOT the whole
// day's totals. We count only active deliveries (and `visibleRiders`, which
// is already gated to active-order riders) so the tiles can't disagree with
// the list/map below.
if (isAllActiveView) {
const activeOrders = allViewOrders.filter(isActiveDelivery);
return {
orders: allViewOrders.length,
orders: activeOrders.length,
riders: visibleRiders.length,
km: allViewOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
profit: allViewOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
km: activeOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
profit: activeOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
label: 'Active Fleet'
};
}
@@ -2183,8 +2186,30 @@ const Dispatch = ({
const pts = buildTripPoints(sorted);
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
// 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 customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`;
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 (
<div
key={o.orderid}
className="rcard active-delivery-card"
className={`adcard${isActive ? ' is-active' : ''}`}
style={{ '--ad-accent': color, animationDelay: `${i * 0.05}s` }}
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 className="adcard-top">
<div className="adcard-avatar" style={{ background: color }}>{initials}</div>
<div className="adcard-titles">
<div className="adcard-customer" title={customer}>{customer}</div>
<div className="adcard-rider" title={riderName}>
<MdTwoWheeler style={{ fontSize: 13, flexShrink: 0 }} />
<span className="adcard-tx">{riderName}</span>
</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}
</div>
</span>
</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
{dropArea && (
<div className="adcard-addr">
<span className="adcard-ic"><MdLocationOn /></span>
<span className="adcard-tx adcard-addr-tx" title={dropArea}>{dropArea}</span>
</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 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>
);
@@ -2729,18 +2792,25 @@ 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.
// Active view drop pins: show ONE flag per active rider — the drop of the
// leg they're currently heading to (getActiveOrder) — so the flag marks
// "where the rider is going" and sits exactly at the end of that rider's
// route line. No other drops are flagged in this view.
// • Focused rider → just that rider's in-progress drop.
// • Overview (no focus) → each visible active rider's in-progress drop.
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);
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)
// null → request in-flight (DON'T draw anything yet — avoids the aerial flash)
// undefined → not yet requested (same as in-flight, wait)
// 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
// The cached per-trip OSRM route (`roadPoints`) covers every stop, so it
// can't represent a single active leg. In the Active view we instead
// build a route scoped to just the in-progress leg and FOLLOW THE ROADS.
//
// Leg endpoints: the rider's CURRENT live GPS position → the drop. The
// live position is preferred because (a) active riders always have a
// 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;
// Aerial fallback (OSRM permanently failed) is rendered dashed so it visually
// reads as an estimate vs. an actual routed road polyline.
@@ -4532,22 +4635,20 @@ const Dispatch = ({
</div>
))
) : isAllActiveView ? (
// Active view: list the in-progress deliveries themselves,
// grouped by rider name then trip/step — NOT rider cards.
// Active view: list exactly ONE card per active rider — the
// 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
.filter(isActiveDelivery)
.slice()
.sort((a, b) => {
const rn = String(a.rider_name || a.ridername || '').localeCompare(
const activeDeliveries = visibleRiders
.map((r) => getActiveOrder(r.orders))
.filter(Boolean)
.sort((a, b) =>
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">
@@ -4561,7 +4662,7 @@ const Dispatch = ({
</div>
);
}
return activeDeliveries.map(renderActiveDeliveryCard);
return <div className="adcard-list">{activeDeliveries.map(renderActiveDeliveryCard)}</div>;
})()
) : (
visibleRiders.map(renderRiderCard)
@@ -4636,7 +4737,7 @@ const Dispatch = ({
{liveRiderLocations
.filter((r) =>
isAllActiveView
? r.status === 'active'
? (r.status === 'active' && activeOrderRiderIdSet.has(String(r.id)))
: riders.some((rd) => String(rd.id) === String(r.id))
)
.filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))