update for the the active section page and the profitability page

This commit is contained in:
2026-06-15 17:42:11 +05:30
parent f50b4b010c
commit ec1957d6ca
6 changed files with 2194 additions and 21 deletions

View File

@@ -52,8 +52,11 @@ import {
MdTimer,
MdCalendarToday,
MdInsights,
MdRefresh
MdRefresh,
MdAttachMoney
} from 'react-icons/md';
import ProfitabilitySection from './ProfitabilitySection';
import ActiveSection from './ActiveSection';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
import {
STATUS_STYLES,
@@ -61,7 +64,9 @@ import {
FINAL_STATUSES,
SKIPPED_STATUSES,
STEP_PALETTE,
stepColor
stepColor,
isActiveDelivery,
getActiveOrder
} from './dispatchShared';
import CompareDataPanel from './CompareDataPanel';
import './Dispatch.css';
@@ -1329,10 +1334,11 @@ const Dispatch = ({
// hub (latitude/longitude/logdate/status). We render those positions as
// markers on the main dispatch map so the operator sees where each rider
// actually is — matching the Reports → Riders Logs page.
const RIDER_LOG_POLL_MS = viewMode === 'all' ? 1_000 : 15_000;
const { data: ridersLocationLogs } = useQuery({
queryKey: [selectedAppLocationId, selectedDate, ''],
queryFn: fetchRidersLogs,
refetchInterval: 15_000,
refetchInterval: RIDER_LOG_POLL_MS,
refetchIntervalInBackground: false,
staleTime: 5 * 1000,
refetchOnWindowFocus: false
@@ -1360,6 +1366,15 @@ const Dispatch = ({
})
.filter(Boolean);
}, [ridersLocationLogs]);
// Set of rider ids whose latest GPS log row is `active` (i.e. on the road
// right now). The "All Active Routes" view (viewMode === 'all') uses this to
// show ONLY currently-active riders — their cards, routes, drop markers and
// live bike markers — and hide everyone who is offline/idle for the slot.
const activeRiderIdSet = useMemo(
() => new Set(liveRiderLocations.filter((r) => r.status === 'active').map((r) => String(r.id))),
[liveRiderLocations]
);
// Default to the slot containing the current wall-clock time. Use a
// fractional hour so 12:45 lands in the 12:30+ slot 2 (not slot 1). If
// the current time falls outside every slot window (e.g. before 8 AM)
@@ -1394,7 +1409,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.
@@ -1459,6 +1481,39 @@ const Dispatch = ({
return liveRows.filter((r) => getRowBatch(r, selectedTimeField, BATCHES) === selectedBatch);
}, [liveRows, selectedBatch, selectedTimeField, BATCHES]);
const totalDailyProfit = useMemo(() => {
let profit = 0;
const slotRiders = {};
liveRows.forEach(r => {
const batch = getRowBatch(r, selectedTimeField, BATCHES);
if (!batch || batch === 'all') return;
const riderKey = String(r.userid || r.rider_id || 'unassigned');
if (riderKey === 'unassigned' || riderKey === '0') return;
if (!slotRiders[batch]) slotRiders[batch] = {};
if (!slotRiders[batch][riderKey]) {
slotRiders[batch][riderKey] = { revenue: 0, kms: 0 };
}
const kms = parseFloat(r.kms || r.actualkms || 0);
slotRiders[batch][riderKey].kms += kms;
slotRiders[batch][riderKey].revenue += (kms <= 8 ? 30 : 30 + (kms - 8) * 6);
});
Object.values(slotRiders).forEach(riderMap => {
Object.values(riderMap).forEach(stats => {
const variableCost = stats.kms * 2.5;
const fixedCost = 166.67;
const totalCost = variableCost + fixedCost;
profit += (stats.revenue - totalCost);
});
});
return profit;
}, [liveRows, selectedTimeField, BATCHES]);
// Reshape flat delivery rows into the zones/riders/orders structure Dispatch consumes.
const liveData = useMemo(() => {
if (!shouldFetchLive) return null;
@@ -1650,6 +1705,63 @@ const Dispatch = ({
? (selectedRiderId ? (riders.find((r) => r.id === selectedRiderId) || null) : null)
: internalFocusedRider;
// "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';
// 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]
);
// 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' && activeOrderRiderIdSet.has(String(r.id)))
.map((r) => [r.lat, r.lon])
: [],
[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.
const activeDeliveryCount = useMemo(
() => (isAllActiveView ? allViewOrders.filter(isActiveDelivery).length : 0),
[isAllActiveView, allViewOrders]
);
// Per-rider canvas renderer for the actual (right) map in Compare mode.
// Single setter used by every interactive site in the UI. In uncontrolled mode it
// updates local state; in controlled mode it only notifies the parent.
@@ -1728,6 +1840,21 @@ const Dispatch = ({
label: 'Focused Kitchen'
};
}
// "All Active Routes": the header must reflect exactly what the list/map
// 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: activeOrders.length,
riders: visibleRiders.length,
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'
};
}
return {
orders: stats.totalOrders,
riders: stats.totalRiders,
@@ -1735,7 +1862,7 @@ const Dispatch = ({
profit: stats.totalProfit,
label: 'Total Fleet'
};
}, [focusedRider, focusedKitchen, stats]);
}, [focusedRider, focusedKitchen, isAllActiveView, allViewOrders, visibleRiders, stats]);
// List of deliveryids tied to the focused rider's orders — used to drive the
// batched per-delivery GPS log fetch for Compare mode. Deduped; ignores rows
@@ -2442,6 +2569,7 @@ const Dispatch = ({
return meters >= 1000 ? `${(meters / 1000).toFixed(1)} km` : `${meters} m`;
};
// Shared rider-card markup, used in the "By Rider" panel and inside the focused-zone detail.
const renderRiderCard = (r, i) => {
const total = r.orders.length;
@@ -3275,7 +3403,21 @@ const Dispatch = ({
onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
><span className="sbt-icon"><MdMap /></span> By Zone</button>
<button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Rider'); setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
<button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span> All Routes</button>
<button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Active Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span>Active</button>
<button
type="button"
className={`sbt sbt-profitability ${viewMode === 'profitability' ? 'active' : ''}`}
onClick={() => {
logger.info('View mode changed: Profitability');
setViewMode('profitability');
handleRiderFocus(null);
setFocusedKitchen(null);
setFocusedZone(null);
}}
title="View overall and rider profitability"
>
<span className="sbt-icon"><MdAttachMoney /></span> Profitability
</button>
<button
type="button"
className={`sbt sbt-rider-info ${viewMode === 'rider-info' ? 'active' : ''}`}
@@ -3483,7 +3625,9 @@ const Dispatch = ({
</div>
)}
{viewMode === 'rider-info' ? (
{viewMode === 'profitability' ? (
<ProfitabilitySection riders={riders} handleRiderFocus={handleRiderFocus} focusedRider={focusedRider} totalDailyProfit={totalDailyProfit} />
) : viewMode === 'rider-info' ? (
<div className="rider-info-mode">
<div className="ri-sidebar">
<div className="ri-sb-head">
@@ -4367,11 +4511,14 @@ const Dispatch = ({
</div>
) : (
<div id="riders-panel">
<div className="ph">{
viewMode === 'zones' ? 'Zone dispatch' :
viewMode === 'kitchens' ? 'Kitchen dispatch' :
'Rider dispatch'
}</div>
{!(isAllActiveView && activeDeliveryCount === 0) && (
<div className="ph">{
viewMode === 'zones' ? 'Zone dispatch' :
viewMode === 'kitchens' ? 'Kitchen dispatch' :
isAllActiveView ? 'Active rider dispatch' :
'Rider dispatch'
}</div>
)}
<div id="rider-cards">
{allOrders.length === 0 && !liveIsFetching ? (
(() => {
@@ -4495,8 +4642,19 @@ const Dispatch = ({
</div>
</div>
))
) : isAllActiveView ? (
<ActiveSection
visibleRiders={visibleRiders}
riders={riders}
focusedStop={focusedStop}
handleRiderFocus={handleRiderFocus}
setFocusedStop={setFocusedStop}
calculateEstMeters={calculateEstMeters}
getRiderColor={getRiderColor}
formatMeters={formatMeters}
/>
) : (
riders.map(renderRiderCard)
visibleRiders.map(renderRiderCard)
)}
</div>
</div>
@@ -4522,7 +4680,7 @@ const Dispatch = ({
<ZoomControl position="bottomright" />
{compareOpen && <CaptureMap targetRef={leftMapRef} />}
<MapAutoResize trigger={`${sidebarCollapsed}|${compareOpen}|${compareDataCollapsed}`} />
<MapController focusedItem={compareFocusItem || ((focusedRider || focusedKitchen) && focusedStop) || focusedRider || focusedKitchen || focusedZone} viewMode={viewMode} orders={allOrders} kitchens={kitchens} locationKey={selectedAppLocationId} />
<MapController focusedItem={compareFocusItem || ((focusedRider || focusedKitchen) && focusedStop) || focusedRider || focusedKitchen || focusedZone} viewMode={viewMode} orders={allViewOrders} kitchens={kitchens} locationKey={selectedAppLocationId} extraPoints={allViewLivePoints} />
{kitchens
.filter(k => Number.isFinite(k.lat) && Number.isFinite(k.lon))
.filter(k => !focusedRider || k.riders.has(focusedRider.id))