update for the the active section page and the profitability page
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user