updates on the dispatch page and the css
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user