updates on the fix

This commit is contained in:
2026-08-12 12:55:18 +05:30
parent bf28249528
commit fabb74c326
15 changed files with 1478 additions and 528 deletions

View File

@@ -5,7 +5,9 @@ import { useState, useEffect, Fragment, useRef, useMemo } from 'react';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { notifyMiler } from 'pages/api/doormileApi';
import { notifyMiler, getConsignmentLogs } from 'pages/api/doormileApi';
import { kalmanSmoothGps, polylineLengthKm } from '../dispatch/dispatchShared';
import { parseDoormileTimestamp } from 'utils/doormileTimestamp';
import { useTheme } from '@mui/material/styles';
import {
MdOutlineDateRange,
@@ -213,7 +215,12 @@ const getRowBatchId = (row) => {
// so a row that lands in Evening Batch on the dispatch page must also land
// in Evening Batch here — otherwise an operator running in IST/PST/etc.
// sees different totals on the two pages for the same underlying rows.
const d = dayjs(t);
// parseDoormileTimestamp (not bare dayjs()) strips a false trailing Z some
// Doormile timestamps carry (see utils/doormileTimestamp.js) — without
// this, a booking assigned "just now" could get shifted +5:30 (IST) past
// every batch window into a gap, silently vanishing from the table even
// though it was correctly assigned server-side.
const d = parseDoormileTimestamp(t);
if (!d.isValid()) return null;
const h = d.hour() + d.minute() / 60;
for (const b of BATCH_OPTIONS) {
@@ -280,6 +287,58 @@ const Deliveries = () => {
const [deliveryamount, setDeliveryamount] = useState();
const [notes, setNotes] = useState('');
const [currentorder, setCurrentorder] = useState({});
// Real Actual KMs for the Update Status dialog, computed from real GPS
// ping history instead of leaving the field permanently blank (there is
// no backend-stored actual-distance field — see fetchDeliveries in
// api.js). Same sort -> Kalman-smooth -> polyline-length pipeline
// Dispatch.js's Compare feature already uses to show a real "actual km"
// figure, just reused here (see dispatchShared.js). Keyed off
// consignmentid, NOT deliveryid/bookingid — GET /admin/consignments/:id/logs
// is consignment-scoped (the same id-class mix-up already fixed in
// updateDeliveryAPI this session).
const { data: computedActualKm } = useQuery({
queryKey: ['deliveryActualKm', currentorder?.consignmentid],
queryFn: async () => {
let rows = [];
try {
rows = (await getConsignmentLogs(currentorder.consignmentid)) || [];
} catch {
return null;
}
const sorted = rows
.map((r) => {
const ts = r?.logdate ? dayjs(r.logdate) : null;
return {
lat: parseFloat(r?.latitude ?? r?.lat),
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
logdate: r?.logdate,
_ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
};
})
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
.sort((a, b) => a._ts - b._ts);
// Fewer than 2 usable pings (order not yet delivered, or no telemetry
// recorded) — a real, expected case, not an error. Leave the field
// to its existing blank/manual-entry behavior.
if (sorted.length < 2) return null;
const smoothed = kalmanSmoothGps(sorted);
const km = polylineLengthKm(smoothed.map((p) => [p.lat, p.lng]));
return km > 0 ? km : null;
},
enabled: dialogopen && !!currentorder?.consignmentid,
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false
});
// Only pre-fill while the field is still empty — never overwrite an
// operator's own edit (or an earlier pre-fill) if this query happens to
// re-run while the dialog is already open.
useEffect(() => {
if (computedActualKm != null && !cumulativekms) {
setCumulativeKms(Number(computedActualKm.toFixed(2)));
}
}, [computedActualKm]);
const [deliverylat, setDeliverylat] = useState('');
const [deliverylong, setDeliverylong] = useState('');
const [currentStatus, setCurrentStatus] = useState('pending');
@@ -311,7 +370,6 @@ const Deliveries = () => {
// batch matching the current UTC hour (the operator is most likely curious
// about "now"); never `null` since there's no longer an "All" option.
const [selectedBatch, setSelectedBatch] = useState(detectInitialBatchId);
const roleid = localStorage.getItem('roleid');
useEffect(() => {
setTenantid(0);
@@ -534,7 +592,21 @@ const Deliveries = () => {
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
});
const countSourceRows = useMemo(() => (countSourceData?.pages || []).flatMap((p) => p.rows || []), [countSourceData]);
// Newest activity first — an order just created and assigned should be the
// very first row an operator sees, not buried wherever GET /admin/bookings
// happens to return it (unconfirmed/unspecified server-side order). Every
// row here has been dispatched at least once (fetchDeliveries filters to
// that), so assigntime (booking.updatedat) is the more recent of the two
// timestamps on a fresh assignment; orderdate (createdat) covers the rare
// row missing it. parseDoormileTimestamp (not bare dayjs()) avoids the
// false-Z parsing bug documented in utils/doormileTimestamp.js.
const countSourceRows = useMemo(() => {
const rows = (countSourceData?.pages || []).flatMap((p) => p.rows || []);
return [...rows].sort(
(a, b) =>
parseDoormileTimestamp(b.assigntime || b.orderdate).valueOf() - parseDoormileTimestamp(a.assigntime || a.orderdate).valueOf()
);
}, [countSourceData]);
useEffect(() => {
if (countHasNext && !countIsFetchingNext) countFetchNext();
@@ -1097,7 +1169,7 @@ const Deliveries = () => {
<Autocomplete
disablePortal
options={locationlist || []}
getOptionLabel={(option) => `${option.locationname} (${option.suburb})` || ''}
getOptionLabel={(option) => (option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '')}
value={locationValue}
PaperComponent={SoftPaper}
sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }}
@@ -1144,7 +1216,10 @@ const Deliveries = () => {
disabled={riderListIsLoading}
options={ridersList}
PaperComponent={SoftPaper}
getOptionLabel={(option) => `${option.displayname || option.authname || ''} (${option.contactno || ''})`}
getOptionLabel={(option) => {
const name = option?.displayname || option?.authname || '';
return option?.contactno ? `${name} (${option.contactno})` : name;
}}
sx={{ flex: { xs: '1 1 100%', sm: '1 1 180px' }, minWidth: { xs: '100%', sm: 180 } }}
onChange={(e, value, reason) => {
if (reason === 'clear') setRiderid(0);
@@ -1494,7 +1569,7 @@ const Deliveries = () => {
<MobileFieldGrid>
<MobileField label="Order / Location" full>
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
{row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.orderid} · {row.deliveryid}
@@ -1546,8 +1621,8 @@ const Deliveries = () => {
</MobileField>
<MobileField label="Kms · plan / act">
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
<Box sx={chipSx('#ef4444')}>{row.kms || 0} km</Box>
<Box sx={chipSx('#10b981')}>{row.cumulativekms || 0} km</Box>
<Box sx={chipSx('#ef4444')}>{Number(row.kms || 0).toFixed(2)} km</Box>
<Box sx={chipSx('#10b981')}>{Number(row.cumulativekms || 0).toFixed(2)} km</Box>
</Stack>
</MobileField>
<MobileField label="Amount · chg / amt">
@@ -1833,7 +1908,7 @@ const Deliveries = () => {
<TableCell align="left">
<Tooltip title="Location Name-Suburb" placement="top">
<Typography variant="subtitle1" noWrap>
{`${row.locationname}-(${row.locationsuburb})`}
{row.locationsuburb ? `${row.locationname}-(${row.locationsuburb})` : row.locationname}
</Typography>
</Tooltip>
@@ -1844,12 +1919,18 @@ const Deliveries = () => {
{row.orderid}
</Typography>
</Tooltip>
{/* express-console-api.md: Doormile timestamps are IST
wall-clock with no timezone marker — .utc() was
reinterpreting that naive string as local time and
shifting it back 5:30, showing the wrong time (and
sometimes the wrong day). Parse bare, matching the
already-correct convention in getRowBatchId above. */}
<Tooltip title="Ordered date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
{dayjs(row.orderdate).format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.orderdate).utc().format('hh:mm A')}
{dayjs(row.orderdate).format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
@@ -1862,44 +1943,62 @@ const Deliveries = () => {
</Tooltip>
<Tooltip title="Delivery date" placement="top">
<Typography noWrap sx={{ fontSize: '12px' }}>
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
{dayjs(row.deliverydate).format('DD/MM/YYYY')}
</Typography>
<Typography noWrap sx={{ fontSize: '11px' }}>
{dayjs(row.deliverydate).utc().format('hh:mm A')}
{dayjs(row.deliverydate).format('hh:mm A')}
</Typography>
</Tooltip>
</Stack>
</Stack>
</TableCell>
{/* pickup */}
<TableCell align="left">
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{/* pickup — capped to the same maxWidth as drop below, with CSS
ellipsis truncation, so one long address can't stretch this
column wider than its neighbour. row.pickuplocation is just
the full raw address (see fetchDeliveries in api.js), not a
pre-shortened name, so the old `.slice(0, 14)` fallback never
actually ran — it was always overridden by the untruncated
value on its left. Full text still available via the tooltip. */}
<TableCell align="left" sx={{ maxWidth: 190 }}>
<Stack sx={{ minWidth: 0 }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{row.pickupcustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.pickupcontactno}
</Typography>
<Tooltip title={row.Pickupaddress} sx={{ whiteSpace: 'nowrap' }}>
<Typography variant="caption" sx={{ color: DT.textMuted }}>
{row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')}
<Tooltip title={row.Pickupaddress || ''}>
<Typography
variant="caption"
sx={{ color: DT.textMuted, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'block' }}
>
{row.pickuplocation || row.Pickupaddress || '—'}
</Typography>
</Tooltip>
</Stack>
</TableCell>
{/* drop */}
<TableCell align="left">
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{/* drop — same maxWidth/truncation treatment as pickup above. */}
<TableCell align="left" sx={{ maxWidth: 190 }}>
<Stack sx={{ minWidth: 0 }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{row.deliverycustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.deliverycontactno}
</Typography>
<Tooltip title={row.deliveryaddress}>
<Typography variant="caption" sx={{ color: DT.textMuted, whiteSpace: 'nowrap' }}>
{row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')}
<Tooltip title={row.deliveryaddress || ''}>
<Typography
variant="caption"
sx={{ color: DT.textMuted, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'block' }}
>
{row.deliverylocation || row.deliveryaddress || '—'}
</Typography>
</Tooltip>
</Stack>
@@ -1973,7 +2072,7 @@ const Deliveries = () => {
minWidth: 75
}}
>
{row.kms || 0} km
{Number(row.kms || 0).toFixed(2)} km
</Box>
</Tooltip>
<Tooltip title="Actual KMS" placement="top">
@@ -1994,7 +2093,7 @@ const Deliveries = () => {
minWidth: 75
}}
>
{row.cumulativekms || 0} km
{Number(row.cumulativekms || 0).toFixed(2)} km
</Box>
</Tooltip>
</Stack>
@@ -2371,27 +2470,29 @@ const Deliveries = () => {
Change Rider
</MenuItem>
)}
{(roleid == 1 || roleid == 2) && (
<MenuItem
onClick={() => {
setKms(selectedRow.kms);
setCumulativeKms(selectedRow.cumulativekms);
setDeliverylat(selectedRow.droplat);
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamt ?? selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#10b981" size={22}>
<MdCheckCircle size={13} />
</AccentAvatar>
Update Status
</MenuItem>
)}
{/* Was gated to roleid 1/2 (staff-only), matching the reference app's
identical gate — opened to every login per explicit decision, so
tenant operators can reach the same Update Status fields staff
already could. */}
<MenuItem
onClick={() => {
setKms(selectedRow.kms);
setCumulativeKms(selectedRow.cumulativekms);
setDeliverylat(selectedRow.droplat);
setDeliverylong(selectedRow.droplon);
setNotes(selectedRow.notes);
setDeliveryamount(selectedRow.deliveryamt ?? selectedRow.deliveryamount);
setUpdateStatus(selectedRow.orderstatus || 'delivered');
setCurrentorder(selectedRow);
setDialogopen(true);
handleMenuClose();
}}
>
<AccentAvatar color="#10b981" size={22}>
<MdCheckCircle size={13} />
</AccentAvatar>
Update Status
</MenuItem>
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
<MenuItem
sx={{ color: '#ef4444 !important' }}
@@ -2539,7 +2640,10 @@ const Deliveries = () => {
fullWidth
options={ridersList}
PaperComponent={SoftPaper}
getOptionLabel={(option) => `${option.displayname || option.authname || ''} (${option.contactno || ''})`}
getOptionLabel={(option) => {
const name = option?.displayname || option?.authname || '';
return option?.contactno ? `${name} (${option.contactno})` : name;
}}
onChange={(e, value) => {
setSelectedRider(value);
logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None');
@@ -2922,6 +3026,7 @@ const Deliveries = () => {
} else {
updateDeliveryMutation.mutate({
deliveryid: currentorder.deliveryid,
consignmentid: currentorder.consignmentid,
orderheaderid: currentorder.orderheaderid,
orderstatus: updateStatus,
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),