updates on the api integration and the design changes on the whole website
This commit is contained in:
@@ -30,8 +30,6 @@ const endIcon = new L.Icon({
|
||||
});
|
||||
|
||||
const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
|
||||
console.log('additionalProps', additionalProps);
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const [routePoints, setRoutePoints] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { React, useState, useEffect, useRef } from 'react';
|
||||
import { React, useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||
|
||||
// material-ui
|
||||
@@ -66,7 +66,7 @@ import { FaCircleCheck } from 'react-icons/fa6';
|
||||
import MapWithRoute from './mapWithRoute';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
|
||||
import { getDashboard, getConsignmentLogs } from 'pages/api/doormileApi';
|
||||
import { getConsignmentLogs } from 'pages/api/doormileApi';
|
||||
import { CSVExport } from 'components/third-party/ReactTable';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
@@ -117,6 +117,10 @@ const edge = (c) => a(c, '55');
|
||||
const BRAND = '#C01227';
|
||||
const BRAND_LIGHT = '#D25463';
|
||||
|
||||
// Haversine-derived kms come back with long floating-point tails
|
||||
// (8.47316323423434532 km) — round to 2 decimals for display everywhere.
|
||||
const formatKm = (value) => Number(value || 0).toFixed(2);
|
||||
|
||||
const SoftPaper = (props) => (
|
||||
<Paper
|
||||
{...props}
|
||||
@@ -250,16 +254,6 @@ export default function OrdersDetails() {
|
||||
const [riderStart, setRiderStart] = useState();
|
||||
const [riderEnd, setRiderEnd] = useState();
|
||||
const [mapTenant, setMapTenant] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
let [total, settotal] = useState(0);
|
||||
let [deliveredLenght, setDeliveredLenght] = useState(0);
|
||||
let [pendingLenght, setPendingLenght] = useState(0);
|
||||
let [cancelLenght, setCancelLenght] = useState(0);
|
||||
let [assignLenght, setAssignLenght] = useState(0);
|
||||
let [pickedLenght, setPickedLenght] = useState(0);
|
||||
let [activeLenght, setActiveLenght] = useState(0);
|
||||
let [arrivesLenght, setArrivedLenght] = useState(0);
|
||||
let [skippedLenght, setSkippedLenght] = useState(0);
|
||||
const [currentStatus, setCurrentStatus] = useState('All');
|
||||
const [locationid, setLocationid] = useState(0);
|
||||
const [tenantid, setTenantid] = useState(0);
|
||||
@@ -270,19 +264,6 @@ export default function OrdersDetails() {
|
||||
const [reportDialog, setReportDialog] = useState(false);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
|
||||
// Map status key (lowercase) → count value from the summary endpoint.
|
||||
const statusCountByKey = {
|
||||
all: total,
|
||||
pending: pendingLenght,
|
||||
accepted: assignLenght,
|
||||
arrived: arrivesLenght,
|
||||
picked: pickedLenght,
|
||||
active: activeLenght,
|
||||
delivered: deliveredLenght,
|
||||
skipped: skippedLenght,
|
||||
cancelled: cancelLenght
|
||||
};
|
||||
|
||||
// Cascading clears so changing a parent filter resets its children.
|
||||
useEffect(() => {
|
||||
setTenantid(0);
|
||||
@@ -353,15 +334,23 @@ export default function OrdersDetails() {
|
||||
calculateTotalDistance(coData);
|
||||
setMapOpen(true);
|
||||
} catch (error) {
|
||||
console.log('getdeliverylogs', error);
|
||||
opentoast('No Logs Found ', 'error', 2000);
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ==============================|| fetchDeliveries (infinite) ||============================== //
|
||||
|
||||
// ==============================|| fetchDeliveries (bulk, auto-drained) ||============================== //
|
||||
// GET /admin/bookings has no status/date/tenant/location/rider filter
|
||||
// params (see express-console-api.md's "Conventions") — fetchDeliveries in
|
||||
// api.js only ever reads rowsPerPage off this query's key, so switching
|
||||
// tabs/tenant/rider used to change nothing about which rows were fetched,
|
||||
// only the cache key. The tab count badges were even further disconnected
|
||||
// — sourced from GET /admin/dashboard, which also takes no params, so they
|
||||
// never matched the (also unfiltered) table either. Fixed the same way
|
||||
// deliveries.js already was: auto-drain every page into one client-side
|
||||
// dataset, then filter/count from that single source so the tab badge and
|
||||
// the table can never disagree.
|
||||
const {
|
||||
data: deliveriesData,
|
||||
isLoading: fetchDeliveriesIsLoading,
|
||||
@@ -371,26 +360,54 @@ export default function OrdersDetails() {
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [
|
||||
'fetchdeliveries',
|
||||
appId,
|
||||
userid,
|
||||
currentStatus,
|
||||
startdate,
|
||||
enddate,
|
||||
rowsPerPage,
|
||||
debouncedSearch,
|
||||
tenantid,
|
||||
locationid,
|
||||
selectedRider?.userid || 0
|
||||
],
|
||||
queryKey: ['fetchdeliveries-all', appId, userid, 'all', startdate, enddate, 200, '', 0, 0, 0],
|
||||
queryFn: fetchDeliveries,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true
|
||||
});
|
||||
const rows = deliveriesData?.pages.flatMap((page) => page.rows) || [];
|
||||
const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]);
|
||||
|
||||
// Unconditional drain — keeps pulling pages regardless of scroll position,
|
||||
// so counts/filters are accurate even before the operator scrolls.
|
||||
useEffect(() => {
|
||||
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const wantStatus = String(currentStatus || '').toLowerCase();
|
||||
const wantTenantId = Number(tenantid) || 0;
|
||||
const wantRiderId = Number(selectedRider?.userid) || 0;
|
||||
const q = String(debouncedSearch || '').trim().toLowerCase();
|
||||
return allRows.filter((r) => {
|
||||
if (wantStatus !== 'all' && String(r.orderstatus || '').toLowerCase() !== wantStatus) return false;
|
||||
if (wantTenantId && Number(r.tenantid) !== wantTenantId) return false;
|
||||
if (wantRiderId && Number(r.userid) !== wantRiderId) return false;
|
||||
if (q) {
|
||||
const hay = [r.deliverycustomer, r.deliveryaddress, r.pickupcustomer, r.pickupaddress, r.orderid, r.tenantname, r.ridername]
|
||||
.map((v) => String(v || '').toLowerCase())
|
||||
.join(' ');
|
||||
if (!hay.includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [allRows, currentStatus, tenantid, selectedRider, debouncedSearch]);
|
||||
|
||||
// Sourced from the exact same allRows the table renders, so a tab's badge
|
||||
// always matches what clicking it actually shows.
|
||||
const statusCountByKey = useMemo(() => {
|
||||
const c = { all: allRows.length, pending: 0, accepted: 0, arrived: 0, picked: 0, active: 0, delivered: 0, skipped: 0, cancelled: 0 };
|
||||
allRows.forEach((r) => {
|
||||
const s = String(r.orderstatus || '').toLowerCase();
|
||||
if (c[s] !== undefined) c[s] += 1;
|
||||
});
|
||||
return c;
|
||||
}, [allRows]);
|
||||
const total = statusCountByKey.all;
|
||||
const pendingLenght = statusCountByKey.pending;
|
||||
const deliveredLenght = statusCountByKey.delivered;
|
||||
const cancelLenght = statusCountByKey.cancelled;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasNextPage) return;
|
||||
@@ -456,33 +473,6 @@ export default function OrdersDetails() {
|
||||
enabled: tenantid !== 0
|
||||
});
|
||||
|
||||
// ==============================|| status summary counts ||============================== //
|
||||
const fetchcount = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = (await getDashboard()) || {};
|
||||
settotal(data.total || 0);
|
||||
setPendingLenght(data.pending || 0);
|
||||
setAssignLenght(data.accepted || 0);
|
||||
setArrivedLenght(data.arrived || 0);
|
||||
setPickedLenght(data.picked || 0);
|
||||
setActiveLenght(data.active || 0);
|
||||
setDeliveredLenght(data.delivered || 0);
|
||||
setSkippedLenght(data.skipped || 0);
|
||||
setCancelLenght(data.cancelled || 0);
|
||||
} catch (err) {
|
||||
enqueueSnackbar(err.message, {
|
||||
variant: 'error',
|
||||
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||
autoHideDuration: 2000
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchcount();
|
||||
}, [appId, startdate, enddate, currentStatus, tenantid, locationid, selectedRider]);
|
||||
|
||||
// CSV export payload — flat schema preserved for backwards compatibility.
|
||||
const csvData = rows?.map((order) => ({
|
||||
@@ -556,7 +546,7 @@ export default function OrdersDetails() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoading ||
|
||||
{(fetchDeliveriesIsLoading ||
|
||||
fetchtenantsIsLoading ||
|
||||
fetchlocationsIsLoading ||
|
||||
logsLoading ||
|
||||
@@ -609,7 +599,7 @@ export default function OrdersDetails() {
|
||||
value={item.value ?? 0}
|
||||
icon={<Icon size={20} />}
|
||||
color={item.color}
|
||||
loading={isLoading}
|
||||
loading={fetchDeliveriesIsLoading}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
@@ -1198,19 +1188,19 @@ export default function OrdersDetails() {
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`}
|
||||
label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
color="#10b981"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${row.cumulativekms ?? 0} km`}
|
||||
label={`${formatKm(row.cumulativekms)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
color="#0ea5e9"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`}
|
||||
label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`}
|
||||
tooltip="Rider KMS"
|
||||
/>
|
||||
</Stack>
|
||||
@@ -1508,12 +1498,17 @@ export default function OrdersDetails() {
|
||||
<StampCell value={row.arrivaltime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StampCell
|
||||
value={row.pickuptime}
|
||||
formatDate={formatDate}
|
||||
formatTime={formatTime}
|
||||
success={row.deliverystatus === 'active'}
|
||||
/>
|
||||
{/* GET /admin/bookings has no per-event timestamps (accepted/
|
||||
arrived/pickup/start/cancel) — only assigntime (updatedat) and
|
||||
deliverydate exist on the mapped row (see fetchDeliveries in
|
||||
api.js), so row.pickuptime is always undefined and this cell
|
||||
always renders the loading skeleton. The old `success` check
|
||||
compared against a `deliverystatus` field that never existed on
|
||||
this row either (the real field is `orderstatus`) — dropped
|
||||
rather than guessed, since orderstatus's confirmed values
|
||||
(pending/accepted/active/delivered/cancelled) have no distinct
|
||||
"picked" state to map it to. */}
|
||||
<StampCell value={row.pickuptime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StampCell value={row.starttime} formatDate={formatDate} formatTime={formatTime} />
|
||||
@@ -1554,19 +1549,19 @@ export default function OrdersDetails() {
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`}
|
||||
label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
color="#10b981"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${row.cumulativekms ?? 0} km`}
|
||||
label={`${formatKm(row.cumulativekms)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
color="#0ea5e9"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`}
|
||||
label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`}
|
||||
tooltip="Rider KMS"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
187
src/pages/nearle/reports/profitability.js
Normal file
187
src/pages/nearle/reports/profitability.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
// material-ui
|
||||
import { Box, Paper, TextField, Typography } from '@mui/material';
|
||||
import { MdCalendarMonth } from 'react-icons/md';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { fetchDeliveries } from 'pages/api/api';
|
||||
import Loader from 'components/Loader';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import ProfitabilitySection, {
|
||||
BASE_REVENUE,
|
||||
BASE_KM_LIMIT,
|
||||
EXTRA_RATE_KM,
|
||||
VARIABLE_RATE_KM,
|
||||
FIXED_COST_PER_SLOT,
|
||||
BATCHES_DEFAULT
|
||||
} from '../dispatch/ProfitabilitySection';
|
||||
import { SKIPPED_STATUSES } from '../dispatch/dispatchShared';
|
||||
|
||||
// ============================================================================
|
||||
// Standalone Profitability report — reference project (nearle_console_express)
|
||||
// has this as its own page (reports/profitability.js); here it only existed
|
||||
// embedded inside Dispatch.js's live map. Rather than rebuild the UI (the
|
||||
// reference re-implements the profit math inline in raw MUI), this reuses
|
||||
// the already-built, already-tested ProfitabilitySection.js component as-is
|
||||
// and just supplies it with independently-fetched data — mirrors how the
|
||||
// reference page ALSO re-fetches and re-groups independently rather than
|
||||
// reusing its own Dispatch page's pipeline (which is entangled with
|
||||
// Leaflet/GPS/zone-bucketing state not needed for a report).
|
||||
//
|
||||
// GET /admin/bookings has no server-side date/zone/status filter params (see
|
||||
// src/pages/api/CLAUDE.md) — bulk-fetched and grouped entirely client-side,
|
||||
// same pattern as reports/ordersDetails.js and deliveries.js.
|
||||
// ============================================================================
|
||||
|
||||
const DT = {
|
||||
radiusCard: 14,
|
||||
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
|
||||
textSecondary: '#64748b',
|
||||
borderSubtle: '#e2e8f0',
|
||||
surface: '#ffffff'
|
||||
};
|
||||
const BRAND = '#C01227';
|
||||
|
||||
// Profit formulas + batch windows imported from ProfitabilitySection.js
|
||||
// (the single source of truth) rather than duplicated here — used to total
|
||||
// across all riders for the daily KPI chip; per-rider numbers are computed
|
||||
// by ProfitabilitySection itself from the same `riders` data, so this must
|
||||
// match exactly or the chip and the table below it would disagree.
|
||||
const orderRevenue = (km) => (km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM);
|
||||
|
||||
const getOrderBatch = (o) => {
|
||||
const t = o.assigntime || o.deliverydate;
|
||||
if (!t || /^\d{4}-\d{2}-\d{2}$/.test(String(t).trim())) return null;
|
||||
const d = dayjs(t);
|
||||
if (!d.isValid()) return null;
|
||||
const hour = d.hour() + d.minute() / 60;
|
||||
const batch = BATCHES_DEFAULT.find((b) => hour >= b.startHour && hour < b.endHour);
|
||||
return batch?.id || null;
|
||||
};
|
||||
|
||||
const Profitability = () => {
|
||||
const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
|
||||
const {
|
||||
data: deliveriesData,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['fetchdeliveries-profitability', 0, 0, 'all', selectedDate, selectedDate, 500, '', 0, 0, 0],
|
||||
queryFn: fetchDeliveries,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
|
||||
});
|
||||
|
||||
const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]);
|
||||
|
||||
// Group into { id, riderName, orders } — flat, no zone/suburb bucketing
|
||||
// (that's a Dispatch-map-specific concern, not needed for a report).
|
||||
// riderkms is what ProfitabilitySection's revenue/cost math actually reads
|
||||
// (see orderRevenue/calcRiderMetrics there) but GET /admin/bookings has no
|
||||
// road-distance field — mapped from the same haversine straight-line `kms`
|
||||
// every other page in this app already uses as the distance proxy.
|
||||
const riders = useMemo(() => {
|
||||
const byRider = new Map();
|
||||
allRows.forEach((row) => {
|
||||
if (!row.userid) return;
|
||||
const key = String(row.userid);
|
||||
if (!byRider.has(key)) {
|
||||
byRider.set(key, { id: key, riderName: row.ridername || `Rider #${key}`, orders: [] });
|
||||
}
|
||||
byRider.get(key).orders.push({ ...row, riderkms: row.kms });
|
||||
});
|
||||
return Array.from(byRider.values());
|
||||
}, [allRows]);
|
||||
|
||||
const totalDailyProfit = useMemo(() => {
|
||||
let profit = 0;
|
||||
riders.forEach((rider) => {
|
||||
let revenue = 0;
|
||||
let kms = 0;
|
||||
const batchesWorked = new Set();
|
||||
rider.orders.forEach((o) => {
|
||||
const status = String(o.orderstatus || '').toLowerCase();
|
||||
if (SKIPPED_STATUSES.has(status)) return;
|
||||
const dateStr = o.assigntime
|
||||
? dayjs(o.assigntime).format('YYYY-MM-DD')
|
||||
: o.deliverydate
|
||||
? dayjs(o.deliverydate).format('YYYY-MM-DD')
|
||||
: null;
|
||||
if (dateStr !== selectedDate) return;
|
||||
const km = parseFloat(o.riderkms || 0);
|
||||
revenue += orderRevenue(km);
|
||||
kms += km;
|
||||
const batch = getOrderBatch(o);
|
||||
if (batch) batchesWorked.add(batch);
|
||||
});
|
||||
const variableCost = kms * VARIABLE_RATE_KM;
|
||||
const fixedCost = Math.min(batchesWorked.size, 3) * FIXED_COST_PER_SLOT;
|
||||
profit += revenue - variableCost - fixedCost;
|
||||
});
|
||||
return profit;
|
||||
}, [riders, selectedDate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <Loader />}
|
||||
|
||||
<PageHeader
|
||||
title="Profitability"
|
||||
subtitle={`Live · ${dayjs(selectedDate).format('DD MMM YYYY')}`}
|
||||
live
|
||||
action={
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
borderRadius: DT.radiusCard + 'px',
|
||||
border: '1px solid',
|
||||
borderColor: DT.borderSubtle,
|
||||
boxShadow: DT.shadowSoft,
|
||||
bgcolor: DT.surface
|
||||
}}
|
||||
>
|
||||
<MdCalendarMonth size={16} color={BRAND} />
|
||||
<TextField
|
||||
type="date"
|
||||
size="small"
|
||||
variant="standard"
|
||||
value={selectedDate}
|
||||
onChange={(e) => e.target.value && setSelectedDate(e.target.value)}
|
||||
InputProps={{ disableUnderline: true }}
|
||||
sx={{ '& input': { fontWeight: 700, color: DT.textSecondary, p: 0 } }}
|
||||
/>
|
||||
</Paper>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box sx={{ mt: { xs: 1.5, md: 2 } }}>
|
||||
{!isLoading && riders.length === 0 ? (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{ p: 6, textAlign: 'center', borderRadius: DT.radiusCard + 'px', border: '1px solid', borderColor: DT.borderSubtle }}
|
||||
>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>No deliveries for {dayjs(selectedDate).format('DD MMM YYYY')}</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<ProfitabilitySection riders={riders} totalDailyProfit={totalDailyProfit} selectedDate={selectedDate} />
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profitability;
|
||||
@@ -214,15 +214,17 @@ export default function RidersSummary() {
|
||||
});
|
||||
|
||||
// Client-side filter across rider name + id.
|
||||
// GET /admin/milers/summary confirmed live: { milerprofileid, userid,
|
||||
// displayname, phone, availabilitystatus, rating, assigned, accepted,
|
||||
// rejected, completed, cancelled, delivered, riderkms, ridercharges, ... }
|
||||
// — no firstname/lastname/charges/deliveryamt/totalorders/pending/kms/
|
||||
// cumulativekms field exists at all, which is why this whole page showed
|
||||
// "—" everywhere.
|
||||
const filteredRows = useMemo(() => {
|
||||
if (!rows) return [];
|
||||
if (!debouncedSearch) return rows;
|
||||
const q = debouncedSearch.toLowerCase().trim();
|
||||
return rows.filter((r) =>
|
||||
[`${r.firstname || ''} ${r.lastname || ''}`, String(r.userid)]
|
||||
.filter(Boolean)
|
||||
.some((field) => String(field).toLowerCase().includes(q))
|
||||
);
|
||||
return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q)));
|
||||
}, [rows, debouncedSearch]);
|
||||
|
||||
// Aggregate KPIs from the loaded summary.
|
||||
@@ -231,9 +233,9 @@ export default function RidersSummary() {
|
||||
return rows.reduce(
|
||||
(acc, r) => {
|
||||
acc.riders += 1;
|
||||
acc.orders += Number(r.totalorders) || 0;
|
||||
acc.orders += Number(r.assigned) || 0;
|
||||
acc.delivered += Number(r.delivered) || 0;
|
||||
acc.amount += Math.max(Number(r.charges) || 0, Number(r.deliveryamt) || 0);
|
||||
acc.amount += Number(r.ridercharges) || 0;
|
||||
return acc;
|
||||
},
|
||||
{ riders: 0, orders: 0, delivered: 0, amount: 0 }
|
||||
@@ -261,7 +263,7 @@ export default function RidersSummary() {
|
||||
// Total Amount sum (preserved from legacy bottom bar).
|
||||
const total = useMemo(() => {
|
||||
if (!rows) return 0;
|
||||
return rows.reduce((sum, row) => sum + (Number(row.deliveryamt) || 0), 0);
|
||||
return rows.reduce((sum, row) => sum + (Number(row.ridercharges) || 0), 0);
|
||||
}, [rows]);
|
||||
|
||||
const KPI_META = [
|
||||
@@ -435,8 +437,8 @@ export default function RidersSummary() {
|
||||
) : (
|
||||
filteredRows.map((row, index) => {
|
||||
const isOpen = openRow === row.userid;
|
||||
const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
|
||||
const riderName = `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || '—';
|
||||
const amount = Number(row.ridercharges) || 0;
|
||||
const riderName = row?.displayname || '—';
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.userid || index}
|
||||
@@ -464,7 +466,7 @@ export default function RidersSummary() {
|
||||
onClick={() => {
|
||||
setSelectedRider({
|
||||
userid: row?.userid,
|
||||
name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}`
|
||||
name: row?.displayname || `Rider ${row?.userid}`
|
||||
});
|
||||
setLogDetails(null);
|
||||
setMapOpen(true);
|
||||
@@ -504,13 +506,13 @@ export default function RidersSummary() {
|
||||
>
|
||||
<MobileFieldGrid columns={2}>
|
||||
<MobileField label="Orders">
|
||||
<CountCell value={row.totalorders} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
<CountCell value={row.assigned} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
</MobileField>
|
||||
<MobileField label="Delivered">
|
||||
<CountCell value={row.delivered} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
</MobileField>
|
||||
<MobileField label="Pending">
|
||||
<CountCell value={row.pending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
<MobileField label="Accepted">
|
||||
<CountCell value={row.accepted} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
</MobileField>
|
||||
<MobileField label="Cancelled">
|
||||
<CountCell value={row.cancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
@@ -519,18 +521,10 @@ export default function RidersSummary() {
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.kms || 0).toFixed(2)} km`}
|
||||
label={`${Number(row.riderkms || 0).toFixed(2)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
</MobileField>
|
||||
<MobileField label="Actual KMS">
|
||||
<MetricPill
|
||||
color="#10b981"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.cumulativekms || 0).toFixed(2)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
</MobileField>
|
||||
<MobileField label="Amount" full>
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
@@ -687,7 +681,7 @@ export default function RidersSummary() {
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Rider</TableCell>
|
||||
<TableCell align="center">Orders</TableCell>
|
||||
<TableCell align="center">Pending</TableCell>
|
||||
<TableCell align="center">Accepted</TableCell>
|
||||
<TableCell align="center">Cancelled</TableCell>
|
||||
<TableCell align="center">Delivered</TableCell>
|
||||
<TableCell align="center">KMS</TableCell>
|
||||
@@ -717,7 +711,7 @@ export default function RidersSummary() {
|
||||
) : (
|
||||
filteredRows.map((row, index) => {
|
||||
const isOpen = openRow === row.userid;
|
||||
const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
|
||||
const amount = Number(row.ridercharges) || 0;
|
||||
return (
|
||||
<React.Fragment key={row.userid || index}>
|
||||
<TableRow
|
||||
@@ -745,7 +739,7 @@ export default function RidersSummary() {
|
||||
</AccentAvatar>
|
||||
<Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
||||
{`${row?.firstname || ''} ${row?.lastname || ''}`.trim() || '—'}
|
||||
{row?.displayname || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||
ID #{row.userid}
|
||||
@@ -755,10 +749,10 @@ export default function RidersSummary() {
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<CountCell value={row.totalorders} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
<CountCell value={row.assigned} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CountCell value={row.pending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
<CountCell value={row.accepted} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CountCell value={row.cancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
@@ -768,20 +762,12 @@ export default function RidersSummary() {
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
<Stack direction="column" spacing={0.5} alignItems="center">
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.kms || 0).toFixed(2)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
color="#10b981"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.cumulativekms || 0).toFixed(2)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
</Stack>
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.riderkms || 0).toFixed(2)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="center">
|
||||
@@ -802,7 +788,7 @@ export default function RidersSummary() {
|
||||
onClick={() => {
|
||||
setSelectedRider({
|
||||
userid: row?.userid,
|
||||
name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}`
|
||||
name: row?.displayname || `Rider ${row?.userid}`
|
||||
});
|
||||
setLogDetails(null);
|
||||
setMapOpen(true);
|
||||
@@ -881,7 +867,7 @@ export default function RidersSummary() {
|
||||
variant="caption"
|
||||
sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }}
|
||||
>
|
||||
Tenant Breakdown · {`${row?.firstname || ''} ${row?.lastname || ''}`.trim()}
|
||||
Tenant Breakdown · {row?.displayname || ''}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user