{
if (rider) handleRiderFocus(rider);
if (canFocus) setFocusedStop({ orderid: o.orderid, lat, lon });
diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css
index f687f03..47bdfd8 100644
--- a/src/pages/nearle/dispatch/Dispatch.css
+++ b/src/pages/nearle/dispatch/Dispatch.css
@@ -10754,13 +10754,23 @@
padding: 4px 0 16px;
}
+@keyframes adcard-in {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
.dispatch-container .adcard {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
cursor: pointer;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ /* Explicit properties so the CSS animation (adcard-in) isn't interrupted by transition: all */
+ transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+ box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+ border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
+ background 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ animation: adcard-in 0.28s cubic-bezier(0.4, 0, 0.2, 1) both;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js
index 35e9bad..43563a4 100644
--- a/src/pages/nearle/dispatch/Dispatch.js
+++ b/src/pages/nearle/dispatch/Dispatch.js
@@ -1613,6 +1613,18 @@ const Dispatch = ({
if (liveHasNextPage && !liveIsFetchingNextPage) liveFetchNextPage();
}, [shouldFetchLive, liveHasNextPage, liveIsFetchingNextPage, liveFetchNextPage]);
+ // Holds the last fully-loaded row set (all pages present). Used below to
+ // prevent the "orders vanish then reappear" flash during background refetches
+ // where TanStack Query fetches pages sequentially and livePagesData temporarily
+ // contains only page 1 while the rest are still loading.
+ const prevCompleteRowsRef = useRef([]);
+
+ // Sticky cache for active orders in the Active Section sidebar. Keys are orderid strings.
+ // Orders are added/refreshed when orderstatus === 'active' and evicted only when the
+ // API explicitly returns a terminal status. This prevents cards from flashing away
+ // during the 15-second poll cycle when the API temporarily omits a row.
+ const stickyActiveRef = useRef({});
+
const liveRows = useMemo(() => {
// Flatten infinite-query pages, then dedupe by orderid. The deliveries API
// can return the same orderid more than once (e.g. when page bookkeeping
@@ -1627,8 +1639,26 @@ const Dispatch = ({
if (key) seen.add(key);
out.push(r);
}
+
+ // Background refetch loads pages one-by-one: page 1 arrives first, then the
+ // auto-pager fetches page 2, 3… While pages are still loading (liveHasNextPage
+ // is true and a fetch is in-flight), "out" is a partial snapshot that is
+ // smaller than the complete dataset we had before. Returning prevCompleteRowsRef
+ // keeps the Active section stable — no cards disappear mid-refetch.
+ // On the very first load prevCompleteRowsRef is empty, so we fall through and
+ // show whatever partial data is already available (correct for initial render).
+ if (liveIsFetching && liveHasNextPage && prevCompleteRowsRef.current.length > 0) {
+ return prevCompleteRowsRef.current;
+ }
+
+ // All pages have arrived — this is a complete snapshot. Persist it so the
+ // next refetch cycle can return it during the partial-pages window above.
+ if (!liveHasNextPage) {
+ prevCompleteRowsRef.current = out;
+ }
+
return out;
- }, [livePagesData]);
+ }, [livePagesData, liveIsFetching, liveHasNextPage]);
// Distinct riders across the WHOLE day's rows — NOT slot-filtered. Used by
// the Rider Info view so the operator can pick any rider regardless of
@@ -1957,6 +1987,56 @@ const Dispatch = ({
: riders,
[isAllActiveView, riders, activeOrderRiderIdSet]
);
+ // Active section sidebar riders — built from the FULL day's unfiltered rows
+ // (liveRows, all batches, no GPS-status gate). Uses a sticky cache so cards
+ // only disappear when the API returns an explicit terminal status (delivered /
+ // cancelled / skipped). Temporary row omissions during the 15-second poll
+ // cycle (single-page refetch, brief API inconsistency) no longer cause cards
+ // to flash away and reappear.
+ const activeViewRiders = useMemo(() => {
+ if (!isAllActiveView) {
+ stickyActiveRef.current = {};
+ return [];
+ }
+
+ const TERMINAL = new Set(['delivered', 'cancelled', 'skipped', 'complete', 'completed']);
+
+ liveRows.forEach((o) => {
+ const id = String(o.orderid);
+ const s = String(o.orderstatus || '').toLowerCase();
+ if (s === 'active') {
+ stickyActiveRef.current[id] = o;
+ } else if (TERMINAL.has(s)) {
+ delete stickyActiveRef.current[id];
+ }
+ // Non-terminal, non-active statuses for a known order are intentionally
+ // ignored — a brief status regression in the poll data must not evict a card.
+ });
+
+ const stableOrders = Object.values(stickyActiveRef.current);
+ if (!stableOrders.length) return [];
+
+ const riderMeta = Object.fromEntries(riders.map((r) => [r.id, r]));
+ const byRider = {};
+ stableOrders.forEach((o) => {
+ const key = String(o.rider_id || o.userid || '');
+ if (!key || key === 'unassigned' || key === '0') return;
+ if (!byRider[key]) {
+ const meta = riderMeta[key];
+ byRider[key] = {
+ id: key,
+ riderName: meta?.riderName || o.rider_name || o.ridername || o.username || `Rider ${key}`,
+ orders: [],
+ color: meta?.color || getStableRiderColor(key)
+ };
+ }
+ if (!byRider[key].orders.some((ex) => String(ex.orderid) === String(o.orderid))) {
+ byRider[key].orders.push(o);
+ }
+ });
+ return Object.values(byRider);
+ }, [isAllActiveView, liveRows, riders]);
+
// 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(
@@ -1969,13 +2049,18 @@ const Dispatch = ({
[isAllActiveView, liveRiderLocations, activeOrderRiderIdSet]
);
- // 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.
+ // Clear the sticky active-order cache whenever the operator switches to a
+ // different date or zone — stale cards from the previous filter must not leak
+ // into the new view.
+ useEffect(() => {
+ stickyActiveRef.current = {};
+ }, [selectedDate, selectedAppLocationId]);
+
+ // Count of in-progress deliveries shown in the Active view list. Derives from
+ // the sticky cache (via activeViewRiders) so header and card list always agree.
const activeDeliveryCount = useMemo(
- () => (isAllActiveView ? allViewOrders.filter(isActiveDelivery).length : 0),
- [isAllActiveView, allViewOrders]
+ () => (isAllActiveView ? activeViewRiders.reduce((sum, r) => sum + r.orders.length, 0) : 0),
+ [isAllActiveView, activeViewRiders]
);
// Per-rider canvas renderer for the actual (right) map in Compare mode.
@@ -2790,7 +2875,7 @@ const Dispatch = ({
html: `