Fix assistant assign flow, cache staleness, and sweep KPI gloss lines
Assistant — assign:
- Order step is a dropdown, not a typed booking number. Exact bookingid /
bookingno matches now win across the whole list before any substring
fallback; row order previously decided the winner, so a short numeric id
could substring-match a different booking than the one on screen.
- Rider step resolves by id or name, so answering by typing no longer
leaves __rider undefined and throws inside executeAssign.
- notified now reflects whether the push actually went out. It was
Boolean(rider.milerprofileid) — "has an id, assume they were told" — so a
notify returning 400 still reported "The rider has been notified."
- Batch assign removed. /hub/bookings/batch-assign is a hub-console
endpoint; an admin JWT is refused (403 on an empty body, so the gate is
on identity). It read as the fast path and could never run.
Assistant — repeat runs:
- Bulk create response is read at data.results, the shape the live endpoint
returns. Only the two flatter shapes were checked, so every bookingid was
discarded while the count came out right by accident.
- Repeat carries each order's previous rider and re-assigns on create, one
admin call per order, one push per rider. Confirm cards say so.
- Message ids are clock-seeded and healed on load; the counter restarted at
1 each session and overwrote itself, giving duplicate React keys.
- Successful writes invalidate the query cache, so the page behind the panel
stops serving rows cached before the write.
Date range picker:
- Fields seed from the range already in force instead of opening blank on a
filtered table.
- Start field takes max={end}; only the end had a bound, so an inverted
range could be emitted and filtered to nothing.
UI:
- Deliveries filters moved outside the table card.
- Side nav keeps one hairline; the border and box-shadow stacked into a
heavy edge.
- KPI gloss lines removed across orders, riders, tenants, customers, hubs,
vehicles, exceptions, app users, orders summary/details, riders summary
and profitability.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,10 +15,6 @@ import {
|
||||
MdCheckCircle,
|
||||
MdCancel,
|
||||
MdInventory2,
|
||||
MdOutlineLocalShipping,
|
||||
MdOutlinePendingActions,
|
||||
MdOutlineCheckCircle,
|
||||
MdOutlineCancel,
|
||||
MdKeyboardArrowDown,
|
||||
MdKeyboardArrowUp,
|
||||
MdArrowDownward,
|
||||
@@ -89,11 +85,10 @@ import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import StatusBadge from 'components/nearle_components/StatusBadge';
|
||||
import StatusTabs from 'components/nearle_components/StatusTabs';
|
||||
import TableLoader from 'components/nearle_components/TableLoader';
|
||||
import { PageShell, KpiGrid, FilterToolbar, DataCard } from 'components/nearle_components/PageLayout';
|
||||
import { PageShell, DataCard } from 'components/nearle_components/PageLayout';
|
||||
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
|
||||
import { useIsNarrowViewport } from 'hooks/useIsNarrowViewport';
|
||||
|
||||
@@ -127,14 +122,6 @@ const STATUS_TABS = [
|
||||
{ status: 'cancelled', countKey: 'cancelLength' }
|
||||
];
|
||||
|
||||
// KPI palette + icons — mirrors the four cards across the top of the page.
|
||||
const KPI_META = [
|
||||
{ key: 'created', label: 'Created Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping },
|
||||
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdOutlinePendingActions },
|
||||
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdOutlineCheckCircle },
|
||||
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdOutlineCancel }
|
||||
];
|
||||
|
||||
// Batches mirror the dispatch page's slot definitions so an operator who
|
||||
// segments the day there sees the same buckets here. Hours are 24h, half-open
|
||||
// [startHour, endHour) — a delivery at exactly endHour falls into the *next*
|
||||
@@ -479,8 +466,7 @@ const Deliveries = () => {
|
||||
setChangeDialogOpen(false);
|
||||
if (res.success) {
|
||||
logger.info(
|
||||
`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${
|
||||
selectedRider?.displayname || selectedRider?.authname
|
||||
`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.displayname || selectedRider?.authname
|
||||
}`
|
||||
);
|
||||
opentoast('Rider Changed Successfully', 'success');
|
||||
@@ -773,7 +759,7 @@ const Deliveries = () => {
|
||||
},
|
||||
{
|
||||
id: 'serial',
|
||||
size: 56,
|
||||
size: 54,
|
||||
minSize: 48,
|
||||
enableSorting: false,
|
||||
meta: { label: '#' },
|
||||
@@ -792,18 +778,13 @@ const Deliveries = () => {
|
||||
id: 'status',
|
||||
accessorKey: 'orderstatus',
|
||||
size: 130,
|
||||
minSize: 100,
|
||||
minSize: 110,
|
||||
meta: { label: 'Status' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Status
|
||||
</Text>
|
||||
),
|
||||
// The badge explains itself when the value did not come from the
|
||||
// booking. Without this an operator comparing the table against
|
||||
// GET /admin/bookings sees "Active" next to
|
||||
// `"status": "Converted_To_Consignment"` and reasonably assumes the
|
||||
// page is wrong — it isn't; the booking simply stops moving at pickup.
|
||||
cell: ({ getValue, row }) =>
|
||||
row.original.statusfromconsignment ? (
|
||||
<Tooltip
|
||||
@@ -818,150 +799,106 @@ const Deliveries = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'tenant',
|
||||
accessorKey: 'tenantname',
|
||||
size: 170,
|
||||
minSize: 130,
|
||||
meta: { label: 'Tenant' },
|
||||
id: 'orderTenant',
|
||||
accessorFn: (row) => `${row.orderid} ${row.tenantname}`,
|
||||
size: 260,
|
||||
minSize: 210,
|
||||
meta: { label: 'Order & Tenant' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Tenant
|
||||
Order & Tenant
|
||||
</Text>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Tooltip content={row.original.tenantadress || ''}>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" maxLines={1}>
|
||||
{row.original.tenantname}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" maxLines={1}>
|
||||
{row.original.tenantsuburb}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" maxLines={1}>
|
||||
{row.original.applocation}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'orderLocation',
|
||||
accessorFn: (row) => row.orderid,
|
||||
size: 240,
|
||||
minSize: 180,
|
||||
enableSorting: false,
|
||||
meta: { label: 'Order / Location' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Order / Location
|
||||
</Text>
|
||||
),
|
||||
// Order and delivery are stacked, not side by side. They used to be two
|
||||
// VStacks inside an HStack, and flex items default to `min-width: auto`
|
||||
// — so the order id (`DM-BK-BC87D8B0-30828`, unbreakable and wider than
|
||||
// half the column) refused to shrink and crushed the delivery stack to
|
||||
// zero width. A zero-width cell doesn't hide its text, it wraps it one
|
||||
// character per line, which is what rendered as a vertical strip of
|
||||
// letters. Two 20-character ids never fitted a 220px column; labelling
|
||||
// them and giving each its own line is what actually fixes it, and the
|
||||
// bare "-" separator between them conveyed nothing.
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
// express-console-api.md: Doormile timestamps are IST wall-clock with
|
||||
// no timezone marker. A Z-suffixed response reinterpreted as local
|
||||
// time shifts by 5:30 — showing the wrong time and sometimes the
|
||||
// wrong day. parseDoormileTimestamp strips that, matching
|
||||
// getRowBatchId above.
|
||||
const ordered = parseDoormileTimestamp(r.orderdate);
|
||||
const delivered = parseDoormileTimestamp(r.deliverydate);
|
||||
return (
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Tooltip content="Location Name-Suburb">
|
||||
<Text maxLines={1}>{r.locationsuburb ? `${r.locationname}-(${r.locationsuburb})` : r.locationname}</Text>
|
||||
<VStack gap={0.5} padding={0} minWidth={0}>
|
||||
<Tooltip content={`Order ID: ${r.orderid} · Hub: ${r.locationname || '—'}`}>
|
||||
<Text weight="bold" maxLines={1} style={{ fontSize: 13, color: DT.textPrimary, letterSpacing: -0.2 }}>
|
||||
{r.orderid}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Order Id">
|
||||
<Text maxLines={1}>{r.orderid}</Text>
|
||||
</Tooltip>
|
||||
<Text type="supporting" color="secondary">
|
||||
Ordered {ordered.format('DD/MM/YYYY · hh:mm A')}
|
||||
</Text>
|
||||
|
||||
<Tooltip content="Delivery Id">
|
||||
<Text maxLines={1}>{r.deliveryid}</Text>
|
||||
</Tooltip>
|
||||
<Text type="supporting" color="secondary">
|
||||
Delivery {delivered.format('DD/MM/YYYY · hh:mm A')}
|
||||
</Text>
|
||||
<HStack gap={1} vAlign="center" minWidth={0}>
|
||||
<Tooltip content={r.tenantadress ? `${r.tenantname} · ${r.tenantadress}` : r.tenantname}>
|
||||
<Text weight="medium" color="secondary" maxLines={1} style={{ fontSize: 12 }}>
|
||||
{r.tenantname}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Text type="supporting" color="secondary" style={{ fontSize: 11, flexShrink: 0 }}>
|
||||
· {ordered.isValid() ? ordered.format('hh:mm A') : '—'}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pickup',
|
||||
accessorKey: 'pickupcustomer',
|
||||
size: 190,
|
||||
minSize: 140,
|
||||
meta: { label: 'Pickup' },
|
||||
id: 'route',
|
||||
accessorFn: (row) => `${row.pickupcustomer} ${row.deliverycustomer}`,
|
||||
size: 320,
|
||||
minSize: 260,
|
||||
meta: { label: 'Route (Pickup → Drop)' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Pickup
|
||||
Route (Pickup → Drop)
|
||||
</Text>
|
||||
),
|
||||
// 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. Full text still
|
||||
// available via the tooltip.
|
||||
cell: ({ row }) => (
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" maxLines={1}>
|
||||
{row.original.pickupcustomer}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.original.pickupcontactno}
|
||||
</Text>
|
||||
<Tooltip content={row.original.Pickupaddress || ''}>
|
||||
<Text type="supporting" color="secondary" maxLines={1}>
|
||||
{row.original.pickuplocation || row.original.Pickupaddress || '—'}
|
||||
</Text>
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const pickupAddr = r.pickupsuburb || r.pickuplocation || r.Pickupaddress || '';
|
||||
const dropAddr = r.deliverysuburb || r.deliverylocation || r.deliveryaddress || '';
|
||||
const routeTooltip = (
|
||||
<div style={{ textAlign: 'left', minWidth: 200, maxWidth: 300, whiteSpace: 'normal', color: '#fff' }}>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '8px' }}>
|
||||
<img src="/pickup-icon.png" alt="Pickup" style={{ width: 18, height: 18, marginTop: 2, flexShrink: 0 }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ fontWeight: 'bold', fontSize: '12px' }}>Pickup: {r.pickupcustomer || 'Pickup'} {r.pickupcontactno ? `(${r.pickupcontactno})` : ''}</span>
|
||||
<span style={{ fontSize: '11px', opacity: 0.9 }}>{r.Pickupaddress || r.pickuplocation || 'No address'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
|
||||
<img src="/drop-icon.png" alt="Drop" style={{ width: 18, height: 18, marginTop: 2, flexShrink: 0 }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ fontWeight: 'bold', fontSize: '12px' }}>Drop: {r.deliverycustomer || 'Drop'} {r.deliverycontactno ? `(${r.deliverycontactno})` : ''}</span>
|
||||
<span style={{ fontSize: '11px', opacity: 0.9 }}>{r.deliveryaddress || r.deliverylocation || 'No address'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={routeTooltip}>
|
||||
<VStack gap={0.5} padding={0} minWidth={0} style={{ cursor: 'pointer' }}>
|
||||
<HStack gap={1} vAlign="center" minWidth={0}>
|
||||
<img src="/pickup-icon.png" alt="Pickup" style={{ width: 16, height: 16, flexShrink: 0 }} />
|
||||
<Text weight="semibold" maxLines={1} style={{ fontSize: 12, flexShrink: 0 }}>
|
||||
{r.pickupcustomer || 'Pickup'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ fontSize: 11 }}>
|
||||
{pickupAddr ? `· ${pickupAddr}` : ''}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack gap={1} vAlign="center" minWidth={0}>
|
||||
<img src="/drop-icon.png" alt="Drop" style={{ width: 17, height: 17, flexShrink: 0 }} />
|
||||
<Text weight="semibold" maxLines={1} style={{ fontSize: 12, flexShrink: 0 }}>
|
||||
{r.deliverycustomer || 'Drop'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ fontSize: 11 }}>
|
||||
{dropAddr ? `· ${dropAddr}` : ''}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Tooltip>
|
||||
</VStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'drop',
|
||||
accessorKey: 'deliverycustomer',
|
||||
size: 190,
|
||||
minSize: 140,
|
||||
meta: { label: 'Drop' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Drop
|
||||
</Text>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" maxLines={1}>
|
||||
{row.original.deliverycustomer}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.original.deliverycontactno}
|
||||
</Text>
|
||||
<Tooltip content={row.original.deliveryaddress || ''}>
|
||||
<Text type="supporting" color="secondary" maxLines={1}>
|
||||
{row.original.deliverylocation || row.original.deliveryaddress || '—'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</VStack>
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rider',
|
||||
accessorKey: 'ridername',
|
||||
size: 180,
|
||||
minSize: 140,
|
||||
size: 190,
|
||||
minSize: 150,
|
||||
sortUndefined: 'last',
|
||||
meta: { label: 'Rider' },
|
||||
header: () => (
|
||||
@@ -972,89 +909,92 @@ const Deliveries = () => {
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return r.ridername ? (
|
||||
<HStack gap={1.5} vAlign="center">
|
||||
<HStack gap={1.5} vAlign="center" minWidth={0}>
|
||||
<AccentAvatar color="#8b5cf6" size={28}>
|
||||
<MdDirectionsBike size={14} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" maxLines={1}>
|
||||
{r.ridername}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{r.userid} · {r.ridercontact || '—'}
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ fontSize: 11 }}>
|
||||
ID #{r.userid} {r.ridercontact ? `· ${r.ridercontact}` : ''}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
) : (
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
<Text type="supporting" weight="medium" color="secondary" style={{ fontStyle: 'italic' }}>
|
||||
Unassigned
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'eta',
|
||||
accessorFn: (row) => row.expecteddeliverytime || undefined,
|
||||
size: 100,
|
||||
minSize: 90,
|
||||
id: 'timingRoute',
|
||||
accessorFn: (row) => row.expecteddeliverytime || row.kms,
|
||||
size: 140,
|
||||
minSize: 110,
|
||||
sortUndefined: 'last',
|
||||
meta: { label: 'ETA' },
|
||||
meta: { label: 'ETA & Distance' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
ETA
|
||||
ETA & Distance
|
||||
</Text>
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const v = getValue();
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const eta = r.expecteddeliverytime ? parseDoormileTimestamp(r.expecteddeliverytime).format('hh:mm A') : null;
|
||||
const kms = Number(r.kms);
|
||||
return (
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{v ? parseDoormileTimestamp(v).format('hh:mm A') : '—'}
|
||||
</Text>
|
||||
<VStack gap={0.5} padding={0} minWidth={0}>
|
||||
<Text weight="bold" style={{ fontSize: 12 }}>
|
||||
{eta ? `ETA ${eta}` : 'ETA —'}
|
||||
</Text>
|
||||
{kms ? (
|
||||
<Text type="supporting" color="secondary" style={{ fontSize: 11 }}>
|
||||
{kms.toFixed(2)} km
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="supporting" color="secondary" style={{ fontSize: 11 }}>
|
||||
—
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'kms',
|
||||
accessorFn: (row) => Number(row.kms) || undefined,
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
id: 'amountQty',
|
||||
accessorFn: (row) => Number(row.deliverycharges) || Number(row.deliveryamt),
|
||||
size: 130,
|
||||
minSize: 100,
|
||||
sortUndefined: 'last',
|
||||
meta: { label: 'Kms' },
|
||||
meta: { label: 'Amount & Qty' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Kms
|
||||
Amount & Qty
|
||||
</Text>
|
||||
),
|
||||
cell: ({ getValue }) => (
|
||||
<VStack gap={0.5} padding={0}>
|
||||
<MetricPill variant="red" label={`${(getValue() ?? 0).toFixed(2)} km`} tooltip="Planned KMS" />
|
||||
</VStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
accessorFn: (row) => Number(row.deliverycharges) || undefined,
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
sortUndefined: 'last',
|
||||
meta: { label: 'Amount' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Amount
|
||||
</Text>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<VStack gap={0.5} padding={0}>
|
||||
<MetricPill variant="red" label={`₹ ${row.original.deliverycharges?.toFixed(2) ?? '0.00'}`} tooltip="Delivery Charge" />
|
||||
<MetricPill variant="green" label={`₹ ${row.original.deliveryamt?.toFixed(2) ?? '0.00'}`} tooltip="Delivery Amount" />
|
||||
</VStack>
|
||||
)
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const amt = r.deliverycharges ?? r.deliveryamt;
|
||||
return (
|
||||
<VStack gap={0.5} padding={0} minWidth={0}>
|
||||
<Text weight="bold" style={{ fontSize: 13, color: DT.textPrimary }}>
|
||||
{amt != null ? `₹ ${Number(amt).toFixed(2)}` : '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" style={{ fontSize: 11 }}>
|
||||
{r.Quantity ? `${r.Quantity} items` : '1 parcel'}
|
||||
</Text>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'notes',
|
||||
accessorKey: 'notes',
|
||||
size: 170,
|
||||
minSize: 120,
|
||||
size: 120,
|
||||
minSize: 90,
|
||||
enableSorting: false,
|
||||
meta: { label: 'Notes' },
|
||||
header: () => (
|
||||
@@ -1069,7 +1009,7 @@ const Deliveries = () => {
|
||||
<Text
|
||||
type="supporting"
|
||||
color="secondary"
|
||||
style={{ maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'inline-block' }}
|
||||
style={{ maxWidth: 110, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'inline-block' }}
|
||||
>
|
||||
{v}
|
||||
</Text>
|
||||
@@ -1081,23 +1021,6 @@ const Deliveries = () => {
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'qty',
|
||||
accessorKey: 'Quantity',
|
||||
size: 80,
|
||||
minSize: 64,
|
||||
meta: { label: 'Qty' },
|
||||
header: () => (
|
||||
<Text type="label" weight="bold" color="secondary">
|
||||
Qty
|
||||
</Text>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Text weight="semibold" style={{ color: row.original.Quantity ? undefined : DT.textMuted, whiteSpace: 'nowrap' }}>
|
||||
{row.original.Quantity || '—'}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
size: 90,
|
||||
@@ -1140,8 +1063,7 @@ const Deliveries = () => {
|
||||
}
|
||||
}
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[tablePagination.pageIndex, tablePagination.pageSize, productCollapse]
|
||||
[tablePagination, buildRowMenuItems, productCollapse]
|
||||
);
|
||||
|
||||
// Two sources feed one visibility slice: whatever the operator toggled in
|
||||
@@ -1343,12 +1265,12 @@ const Deliveries = () => {
|
||||
const errorMessage = fetchCountIsError
|
||||
? `Error fetching count summary: ${fetchCountError?.message}`
|
||||
: ridersListIsError
|
||||
? `Error fetching riders: ${ridersListError?.message}`
|
||||
: fetchtenantsIsError
|
||||
? `Error tenant list: ${fetchtenantsError?.message}`
|
||||
: fetchlocationsIsError
|
||||
? `Error location list: ${fetchlocationsError?.message}`
|
||||
: null;
|
||||
? `Error fetching riders: ${ridersListError?.message}`
|
||||
: fetchtenantsIsError
|
||||
? `Error tenant list: ${fetchtenantsError?.message}`
|
||||
: fetchlocationsIsError
|
||||
? `Error location list: ${fetchlocationsError?.message}`
|
||||
: null;
|
||||
|
||||
if (errorMessage) {
|
||||
OpenToast(errorMessage, 'error', 2000);
|
||||
@@ -1366,14 +1288,33 @@ const Deliveries = () => {
|
||||
});
|
||||
|
||||
// Trims the redundant " Batch" off BATCH_OPTIONS' own label (which stays
|
||||
// full elsewhere, e.g. the empty-state message) — "Afternoon Batch · 89"
|
||||
// was overflowing/wrapping inside the selector's compact trigger, squeezed
|
||||
// to a 200px FilterToolbar track alongside four other filters.
|
||||
// full elsewhere, e.g. the empty-state message) — "Afternoon Batch" was
|
||||
// overflowing/wrapping inside the selector's compact trigger, squeezed to a
|
||||
// 200px FilterToolbar track alongside four other filters.
|
||||
//
|
||||
// The count is NOT in the label any more. It used to be appended as
|
||||
// "Evening · 0", which put a number in the closed trigger where it read as
|
||||
// part of the batch's name, and — on any batch with nothing in it — put a
|
||||
// bare "· 0" in the filter row. A count belongs beside the option it counts,
|
||||
// as a badge, and a zero is better said by absence than by a "0".
|
||||
const batchSelectorOptions = BATCH_OPTIONS.map((b) => ({
|
||||
value: b.id,
|
||||
label: `${b.label.replace(/ Batch$/, '')} · ${batchTotals[b.id] ?? 0}`
|
||||
label: b.label.replace(/ Batch$/, '')
|
||||
}));
|
||||
|
||||
// The count rides in the open menu only. `renderOption` receives the option
|
||||
// data, which carries `value` but not the total, so it is looked up here
|
||||
// rather than smuggled through the label.
|
||||
const renderBatchOption = (option) => {
|
||||
const total = batchTotals[option.value] ?? 0;
|
||||
return (
|
||||
<HStack gap={2} vAlign="center" justify="between" style={{ width: '100%', minWidth: 0 }}>
|
||||
<Text>{option.label}</Text>
|
||||
{total > 0 && <Badge variant="neutral" label={total} />}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
const renderProductDetails = () => (
|
||||
<Card padding={0} elevation="none" style={{ margin: 16 }}>
|
||||
<HStack gap={2} vAlign="center" padding={2} style={{ borderBottom: `1px solid ${DT.divider}`, background: tint(BRAND) }}>
|
||||
@@ -1444,9 +1385,8 @@ const Deliveries = () => {
|
||||
const emptyDescription =
|
||||
selectedBatch === 'all'
|
||||
? `No ${(getStatusMeta(currentStatus)?.label || tabstatus).toLowerCase()} orders for this filter.`
|
||||
: `No ${(getStatusMeta(currentStatus)?.label || tabstatus).toLowerCase()} orders in ${
|
||||
BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'
|
||||
}.`;
|
||||
: `No ${(getStatusMeta(currentStatus)?.label || tabstatus).toLowerCase()} orders in ${BATCH_OPTIONS.find((b) => b.id === selectedBatch)?.label || 'this batch'
|
||||
}.`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1454,7 +1394,7 @@ const Deliveries = () => {
|
||||
<Loader />
|
||||
)}
|
||||
|
||||
<PageShell fill>
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title="Deliveries"
|
||||
subtitle={`Live · ${locaName || 'All Zones'}`}
|
||||
@@ -1470,48 +1410,66 @@ const Deliveries = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* All four tiles now read off the same countSourceRows-derived data
|
||||
the StatusTabs pills use (batchTotals/batchCounts), instead of
|
||||
three of them reading a separate, unscoped `fetchCountAPI` call —
|
||||
fetchCountAPI ignores every argument passed to it (see api.js) and
|
||||
returns an ALL-TIME, un-batched, un-dated total, which is why
|
||||
"Pending Orders" could show a number the Pending tab itself
|
||||
disagreed with. Same fix orders.js's KPI row already applies:
|
||||
a tile and its tab must read the same source so they can't drift. */}
|
||||
<KpiGrid>
|
||||
{[
|
||||
{ ...KPI_META[0], value: batchTotals.all },
|
||||
{ ...KPI_META[1], value: batchCounts.uncoveredLength },
|
||||
{ ...KPI_META[2], value: batchCounts.coveredLength },
|
||||
{ ...KPI_META[3], value: batchCounts.cancelLength }
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<StatCard
|
||||
key={item.key}
|
||||
title={item.label}
|
||||
value={item.value ?? 0}
|
||||
icon={<Icon size={20} />}
|
||||
color={item.color}
|
||||
loading={countSourceIsLoading}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</KpiGrid>
|
||||
{/* Status segment cards at the top of the page */}
|
||||
<StatusTabs
|
||||
value={currentStatus}
|
||||
onChange={(status) => {
|
||||
const idx = STATUS_TABS.findIndex((t) => t.status === status);
|
||||
if (idx !== -1) handleChangetab(idx);
|
||||
}}
|
||||
tabs={[
|
||||
...STATUS_TABS.map((t) => ({ status: t.status, count: batchCounts[t.countKey] ?? 0 })),
|
||||
...(batchCounts.otherLength > 0
|
||||
? [{ status: 'other', label: `Other (${unmappedStatuses.join(', ')})`, count: batchCounts.otherLength }]
|
||||
: [])
|
||||
]}
|
||||
size="sm"
|
||||
hasDivider={false}
|
||||
hideHint={true}
|
||||
/>
|
||||
|
||||
<DataCard
|
||||
fill
|
||||
toolbar={
|
||||
<FilterToolbar max={5} minWidth={200}>
|
||||
{/* Filters live OUTSIDE the table card.
|
||||
Inside it, the batch/date/tenant/location/rider controls read as
|
||||
part of the table's own chrome — a header band belonging to the
|
||||
rows — when they actually decide WHICH rows exist. Lifting them
|
||||
out makes the card start at the data, and the controls read as
|
||||
what they are: page-level filters acting on the card below. */}
|
||||
<div
|
||||
className="dt-deliveries-toolbar"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
padding: '0',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: '1 1 130px', minWidth: 120, maxWidth: 180 }}>
|
||||
<Selector
|
||||
label="Batch"
|
||||
isLabelHidden
|
||||
options={batchSelectorOptions}
|
||||
renderOption={renderBatchOption}
|
||||
value={selectedBatch}
|
||||
onChange={(v) => v && setSelectedBatch(v)}
|
||||
placeholder="Batch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<Button
|
||||
label={
|
||||
startdate && enddate ? `${dayjs(startdate).format('DD MMM')} – ${dayjs(enddate).format('DD MMM')}` : 'Select date range'
|
||||
@@ -1520,7 +1478,9 @@ const Deliveries = () => {
|
||||
icon={<MdOutlineDateRange size={16} />}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '1 1 160px', minWidth: 155, maxWidth: 240 }}>
|
||||
<Selector
|
||||
label="Tenant"
|
||||
isLabelHidden
|
||||
@@ -1540,7 +1500,9 @@ const Deliveries = () => {
|
||||
isDisabled={appId == null}
|
||||
disabledMessage="Please select your zone first!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '1 1 160px', minWidth: 155, maxWidth: 240 }}>
|
||||
<Selector
|
||||
label="Location"
|
||||
isLabelHidden
|
||||
@@ -1560,7 +1522,9 @@ const Deliveries = () => {
|
||||
isDisabled={!tenantid}
|
||||
disabledMessage={appId == null ? 'Please select your Zone and Tenant first!' : 'Please select your Tenant first!'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '1 1 160px', minWidth: 155, maxWidth: 240 }}>
|
||||
<Selector
|
||||
label="Rider"
|
||||
isLabelHidden
|
||||
@@ -1573,61 +1537,51 @@ const Deliveries = () => {
|
||||
isDisabled={riderListIsLoading || appId == null}
|
||||
disabledMessage="Please select your zone first!"
|
||||
/>
|
||||
</FilterToolbar>
|
||||
}
|
||||
tabs={
|
||||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap">
|
||||
<StatusTabs
|
||||
value={currentStatus}
|
||||
onChange={(status) => {
|
||||
const idx = STATUS_TABS.findIndex((t) => t.status === status);
|
||||
if (idx !== -1) handleChangetab(idx);
|
||||
}}
|
||||
tabs={[
|
||||
...STATUS_TABS.map((t) => ({ status: t.status, count: batchCounts[t.countKey] ?? 0 })),
|
||||
// Only ever rendered when a status arrived that this page has
|
||||
// no tab for. Its presence is the signal that the mapping in
|
||||
// api.js needs extending — the rows are reachable meanwhile.
|
||||
...(batchCounts.otherLength > 0
|
||||
? [{ status: 'other', label: `Other (${unmappedStatuses.join(', ')})`, count: batchCounts.otherLength }]
|
||||
: [])
|
||||
]}
|
||||
size="sm"
|
||||
hasDivider={false}
|
||||
/>
|
||||
<HStack gap={2} vAlign="center">
|
||||
<VStack maxWidth={260} width="100%" padding={0} gap={0}>
|
||||
<DebounceSearchBar value={searchword} onChange={setSearchword} onDebouncedChange={setDebouncedSearch} />
|
||||
</VStack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columns menu — driven by the same `columnVisibility` slice
|
||||
the tab uses for select/action, so there is one source of
|
||||
truth for "is this column showing" (same pattern as
|
||||
orders.js). Only rendered for the desktop table. */}
|
||||
{!isMobile && (
|
||||
<DropdownMenu
|
||||
button={{ label: 'Columns', variant: 'secondary', size: 'sm', icon: <MdViewColumn size={16} /> }}
|
||||
menuWidth={200}
|
||||
placement="below"
|
||||
>
|
||||
{table
|
||||
.getAllLeafColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
label={column.columnDef.meta?.label ?? column.id}
|
||||
value={column.getIsVisible()}
|
||||
onChange={(checked) => column.toggleVisibility(checked)}
|
||||
hasCloseOnSelect={false}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
flex: '0 1 auto',
|
||||
minWidth: 260,
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', minWidth: 180, maxWidth: 260 }}>
|
||||
<DebounceSearchBar value={searchword} onChange={setSearchword} onDebouncedChange={setDebouncedSearch} />
|
||||
</div>
|
||||
|
||||
{/* Columns menu — driven by the same `columnVisibility` slice
|
||||
the tab uses for select/action, so there is one source of
|
||||
truth for "is this column showing" (same pattern as
|
||||
orders.js). Only rendered for the desktop table. */}
|
||||
{!isMobile && (
|
||||
<DropdownMenu
|
||||
button={{ label: 'Columns', variant: 'secondary', size: 'sm', icon: <MdViewColumn size={16} /> }}
|
||||
menuWidth={200}
|
||||
placement="below"
|
||||
>
|
||||
{table
|
||||
.getAllLeafColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
label={column.columnDef.meta?.label ?? column.id}
|
||||
value={column.getIsVisible()}
|
||||
onChange={(checked) => column.toggleVisibility(checked)}
|
||||
hasCloseOnSelect={false}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataCard>
|
||||
{isMobile ? (
|
||||
<MobileCardList scroll>
|
||||
{filteredRows.length === 0 && !loading1 && !countSourceLoading && (
|
||||
@@ -1815,9 +1769,11 @@ const Deliveries = () => {
|
||||
Widths come from header.getSize(), the columnSizing slice
|
||||
the drag handles write to. */}
|
||||
<Table
|
||||
density="compact"
|
||||
dividers="none"
|
||||
density="balanced"
|
||||
dividers="rows"
|
||||
hasHover
|
||||
verticalAlign="middle"
|
||||
aria-label="Deliveries"
|
||||
style={{ tableLayout: 'fixed', width: '100%', minWidth: table.getTotalSize() }}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -1850,11 +1806,11 @@ const Deliveries = () => {
|
||||
onKeyDown={
|
||||
canSort
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
header.column.toggleSorting();
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
header.column.toggleSorting();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined}
|
||||
@@ -2082,8 +2038,7 @@ const Deliveries = () => {
|
||||
onClick={() => {
|
||||
setLoading1(true);
|
||||
logger.info(
|
||||
`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${
|
||||
selectedRider?.displayname || selectedRider?.authname
|
||||
`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.displayname || selectedRider?.authname
|
||||
}`
|
||||
);
|
||||
changeRiderMutation.mutate({ selectedRider, selectedRow });
|
||||
@@ -2107,6 +2062,9 @@ const Deliveries = () => {
|
||||
open={open}
|
||||
toggle={() => setOpen(!open)}
|
||||
id="daterange1"
|
||||
// So the fields open showing the window already in force, rather
|
||||
// than blank on an actively filtered table.
|
||||
value={{ startDate: startdate || undefined, endDate: enddate || undefined }}
|
||||
onChange={(range) => {
|
||||
if (range.label === 'All') {
|
||||
setStartdate('');
|
||||
|
||||
Reference in New Issue
Block a user