From b6f5cdcaa8294e12960b857626f1b7df6d70fbc5 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Tue, 9 Jun 2026 18:08:11 +0530 Subject: [PATCH] updates on the dispatch page and the css --- src/pages/nearle/dispatch/Dispatch.css | 28 ++- src/pages/nearle/dispatch/Dispatch.js | 295 +++++++++++++++++-------- 2 files changed, 222 insertions(+), 101 deletions(-) diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index 04c84f0..cbd69c1 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -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; diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index 21f0b14..25704f7 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -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 (
{ if (rider) handleRiderFocus(rider); if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon }); }} - style={{ animationDelay: `${i * 0.05}s` }} > -
-
-
-
{customer}
-
- {o.rider_name || o.ridername || 'Unassigned'}{dropArea ? ` · ${dropArea}` : ''} +
+
{initials}
+
+
{customer}
+
+ + {riderName}
-
+ {statusStyle.label} -
+
-
- {o.pickupcustomer && {o.pickupcustomer}} - {parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km - {estMeters !== null && ( - - {formatMeters(estMeters)} to drop + + {dropArea && ( +
+ + {dropArea} +
+ )} + +
+ {o.pickupcustomer ? ( + + + {o.pickupcustomer} + ) : ( + )} + + + + {parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km + + {estMeters !== null && ( + + + {formatMeters(estMeters)} + + )} +
); @@ -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 = ({
)) ) : 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 (
@@ -4561,7 +4662,7 @@ const Dispatch = ({
); } - return activeDeliveries.map(renderActiveDeliveryCard); + return
{activeDeliveries.map(renderActiveDeliveryCard)}
; })() ) : ( 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))