updatess on the ui changs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@@ -15,15 +15,34 @@ import {
|
||||
MdOutlineLocalShipping,
|
||||
MdOutlineCheckCircle,
|
||||
MdOutlinePendingActions,
|
||||
MdOutlineCancel
|
||||
MdOutlineCancel,
|
||||
MdArrowUpward,
|
||||
MdArrowDownward,
|
||||
MdUnfoldMore
|
||||
} from 'react-icons/md';
|
||||
import { FaCircleCheck } from 'react-icons/fa6';
|
||||
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
|
||||
// TanStack Table v9 — same engine orders.js/riders.js/Tenants.js use, so
|
||||
// sorting/resizing/pagination behave identically across the console.
|
||||
import {
|
||||
useTable,
|
||||
flexRender,
|
||||
tableFeatures,
|
||||
rowSortingFeature,
|
||||
createSortedRowModel,
|
||||
sortFns,
|
||||
columnSizingFeature,
|
||||
columnResizingFeature,
|
||||
rowPaginationFeature,
|
||||
createPaginatedRowModel
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Center } from '@astryxdesign/core/Center';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||||
@@ -34,6 +53,7 @@ import { Selector } from '@astryxdesign/core/Selector';
|
||||
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
||||
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Pagination } from '@astryxdesign/core/Pagination';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||||
|
||||
import MapWithRoute from './mapWithRoute';
|
||||
@@ -119,14 +139,43 @@ const StampCell = ({ value, formatDate, formatTime, success }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// TanStack Table v9 registers features explicitly — only sorting/resizing/
|
||||
// pagination ship here (no row-selection or column-visibility, since this
|
||||
// table has no bulk-select or column-hide flow).
|
||||
const ORDERS_DETAILS_TABLE_FEATURES = tableFeatures({
|
||||
rowSortingFeature,
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
sortFns,
|
||||
columnSizingFeature,
|
||||
columnResizingFeature,
|
||||
rowPaginationFeature,
|
||||
paginatedRowModel: createPaginatedRowModel()
|
||||
});
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
|
||||
|
||||
// Uppercase micro-label for the table's column headings — same convention
|
||||
// orders.js's/riders.js's/Tenants.js's tables use.
|
||||
const SectionLabel = ({ children }) => (
|
||||
<Text type="label" weight="bold" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// ==============================|| Orders Details ||============================== //
|
||||
|
||||
export default function OrdersDetails() {
|
||||
const isMobile = useIsNarrowViewport();
|
||||
const loadMoreRef = useRef();
|
||||
const userid = localStorage.getItem('userid');
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(50);
|
||||
// Table sorting/resizing/pagination state — mirrors orders.js's/riders.js's
|
||||
// slices. Replaces the old page/rowsPerPage pair, which was never actually
|
||||
// wired to real pagination (the query key hardcodes 200 rows/page server-
|
||||
// side and auto-drains every page unconditionally — see the fetchDeliveries
|
||||
// useInfiniteQuery below); page/rowsPerPage only fed the "#" serial number,
|
||||
// always at page 0.
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 });
|
||||
const [columnSizing, setColumnSizing] = useState({});
|
||||
const [locaName, setLocoName] = useState('All');
|
||||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||
@@ -297,36 +346,13 @@ export default function OrdersDetails() {
|
||||
const deliveredLenght = statusCountByKey.delivered;
|
||||
const cancelLenght = statusCountByKey.cancelled;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasNextPage) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
{
|
||||
// The sentinel lives AFTER </TableScroll>, not inside it. An
|
||||
// IntersectionObserver only reports on targets that are descendants of
|
||||
// its root, so rooting this at `.dt-table-scroll` meant the callback
|
||||
// never fired on desktop and the report stopped at page 1. (It happened
|
||||
// to work on mobile, where MobileCardList renders instead and the
|
||||
// querySelector returned null — falling back to the viewport.)
|
||||
//
|
||||
// The page scrolls inside AppShell's own container, not the window,
|
||||
// because MainLayout mounts AppShell with height="fill" — so
|
||||
// `.astryx-layout-content` is the real scroll root. Same target
|
||||
// orders.js uses; see the comment in layout/MainLayout/index.js.
|
||||
root: document.querySelector('.astryx-layout-content'),
|
||||
rootMargin: '200px',
|
||||
threshold: 0
|
||||
}
|
||||
);
|
||||
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
||||
return () => {
|
||||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||||
};
|
||||
}, [hasNextPage, fetchNextPage]);
|
||||
// The scroll-to-reveal sentinel + IntersectionObserver that used to live
|
||||
// here was already dead: the unconditional drain effect above (`if
|
||||
// (hasNextPage && !isFetchingNextPage) fetchNextPage()`) exhausts every
|
||||
// page as soon as data arrives, regardless of scroll position, so
|
||||
// `hasNextPage` is almost always already false by the time a user could
|
||||
// scroll to the sentinel. Real client-side pagination (via the `table`
|
||||
// instance below) replaces it instead.
|
||||
|
||||
// ==============================|| Tenant / Location / Rider lookups ||============================== //
|
||||
|
||||
@@ -457,6 +483,349 @@ export default function OrdersDetails() {
|
||||
{ key: 'cancelled', label: 'Cancelled', color: STATUS.cancelled, icon: MdOutlineCancel, value: cancelLenght }
|
||||
];
|
||||
|
||||
// A tab/zone/tenant/location/rider/date/search change re-scopes the list,
|
||||
// so page 1 is the only page that still means anything — same reset
|
||||
// orders.js/riders.js do on their own tables.
|
||||
useEffect(() => {
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}, [currentStatus, appId, tenantid, locationid, selectedRider, startdate, enddate, debouncedSearch]);
|
||||
|
||||
// The column model — everything a column IS declared once here instead of
|
||||
// a hardcoded header row + hand-written body row, same shape as
|
||||
// orders.js's/riders.js's/Tenants.js's `columns`. Cell JSX below is a
|
||||
// straight port of what each column already rendered — only the wiring
|
||||
// changed (columns own their cell now, instead of a shared body-row map).
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'serial',
|
||||
size: 44,
|
||||
minSize: 36,
|
||||
maxSize: 52,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
meta: { label: '#' },
|
||||
header: () => <SectionLabel>#</SectionLabel>,
|
||||
cell: ({ table: t, row }) => (
|
||||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||||
{String(pagination.pageIndex * pagination.pageSize + t.getRowModel().rows.indexOf(row) + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'map',
|
||||
size: 60,
|
||||
minSize: 50,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
meta: { label: 'Map' },
|
||||
header: () => <SectionLabel>Map</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const isDelivered = r.orderstatus === 'delivered';
|
||||
return (
|
||||
<IconButton
|
||||
label={isDelivered ? 'View rider route' : 'Available for delivered orders'}
|
||||
tooltip={isDelivered ? 'View rider route' : 'Available for delivered orders'}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdMap size={14} />}
|
||||
isDisabled={!isDelivered}
|
||||
onClick={() => {
|
||||
if (isDelivered) {
|
||||
getdeliverylogs(r.deliveryid);
|
||||
setMapTenant(r);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'client',
|
||||
accessorKey: 'tenantname',
|
||||
size: 200,
|
||||
minSize: 170,
|
||||
meta: { label: 'Client' },
|
||||
header: () => <SectionLabel>Client</SectionLabel>,
|
||||
cell: ({ getValue, row }) => (
|
||||
<HStack gap={1.5} vAlign="start">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdGroups size={16} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{getValue()}
|
||||
</Text>
|
||||
<Tooltip content="Order ID">
|
||||
<Text type="supporting" color="secondary" weight="bold">
|
||||
#{row.original.orderid}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Text type="supporting" color="secondary">
|
||||
{parseDoormileTimestamp(row.original.deliverydate).format('DD/MM/YYYY · hh:mm A')}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'pickup',
|
||||
accessorKey: 'pickupcustomer',
|
||||
size: 220,
|
||||
minSize: 180,
|
||||
meta: { label: 'Pickup' },
|
||||
header: () => <SectionLabel>Pickup</SectionLabel>,
|
||||
cell: ({ getValue, row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{getValue() || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{r.pickupcontactno}
|
||||
</Text>
|
||||
<Tooltip content={r.Pickupaddress || r.pickupsuburb || ''}>
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ cursor: 'pointer' }}>
|
||||
{r.pickupsuburb || (r.Pickupaddress ? r.Pickupaddress.slice(0, 22) + '…' : '')}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
{r.applocation && <Badge variant="green" icon={<MdPlace size={11} />} label={r.applocation} />}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'drop',
|
||||
accessorKey: 'deliverycustomer',
|
||||
size: 220,
|
||||
minSize: 180,
|
||||
meta: { label: 'Drop' },
|
||||
header: () => <SectionLabel>Drop</SectionLabel>,
|
||||
cell: ({ getValue, row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{getValue() || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{r.deliverycontactno}
|
||||
</Text>
|
||||
<Tooltip content={r.deliveryaddress || ''}>
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ cursor: 'pointer' }}>
|
||||
{r.deliverysuburb || (r.deliveryaddress ? r.deliveryaddress.slice(0, 22) + '…' : '')}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'statusRider',
|
||||
accessorKey: 'orderstatus',
|
||||
size: 160,
|
||||
minSize: 130,
|
||||
meta: { label: 'Status / Rider' },
|
||||
header: () => <SectionLabel>Status / Rider</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<VStack gap={1} padding={0}>
|
||||
<StatusBadge status={r.orderstatus} />
|
||||
{r.ridername && (
|
||||
<HStack gap={1} vAlign="center">
|
||||
<MdDirectionsBike size={12} color={DT.textMuted} />
|
||||
<Tooltip content={`Rider ID: ${r.userid}`}>
|
||||
<Text weight="bold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{r.ridername}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'assigned',
|
||||
accessorKey: 'assigntime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Assigned' },
|
||||
header: () => <SectionLabel>Assigned</SectionLabel>,
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'accepted',
|
||||
accessorKey: 'acceptedtime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Accepted' },
|
||||
header: () => <SectionLabel>Accepted</SectionLabel>,
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'arrived',
|
||||
accessorKey: 'arrivaltime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Arrived' },
|
||||
header: () => <SectionLabel>Arrived</SectionLabel>,
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'picked',
|
||||
accessorKey: 'pickuptime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Picked' },
|
||||
header: () => <SectionLabel>Picked</SectionLabel>,
|
||||
// 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
|
||||
// muted dash. 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 have no distinct "picked" state.
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'active',
|
||||
accessorKey: 'starttime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Active' },
|
||||
header: () => <SectionLabel>Active</SectionLabel>,
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'delivered',
|
||||
accessorKey: 'deliverytime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Delivered' },
|
||||
header: () => <SectionLabel>Delivered</SectionLabel>,
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'cancelled',
|
||||
accessorKey: 'canceltime',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Cancelled' },
|
||||
header: () => <SectionLabel>Cancelled</SectionLabel>,
|
||||
cell: ({ getValue }) => <StampCell value={getValue()} formatDate={formatDate} formatTime={formatTime} />
|
||||
},
|
||||
{
|
||||
id: 'notes',
|
||||
size: 180,
|
||||
minSize: 140,
|
||||
maxSize: 220,
|
||||
enableSorting: false,
|
||||
meta: { label: 'Notes' },
|
||||
header: () => <SectionLabel>Notes</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return r.ordernotes ? (
|
||||
<HStack gap={1} vAlign="start">
|
||||
<MdNoteAlt size={12} color={DT.textMuted} style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<Text type="supporting" color="secondary" maxLines={2}>
|
||||
{r.ordernotes}
|
||||
</Text>
|
||||
</HStack>
|
||||
) : (
|
||||
<Text type="supporting" color="secondary">
|
||||
—
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'kms',
|
||||
accessorKey: 'kms',
|
||||
size: 150,
|
||||
minSize: 130,
|
||||
meta: { label: 'Kms', align: 'center' },
|
||||
header: () => <SectionLabel>Kms</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const cancelled = r.orderstatus === 'cancelled';
|
||||
return (
|
||||
<VStack gap={1} padding={0} vAlign="center">
|
||||
<MetricPill
|
||||
variant="red"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={cancelled || r.kms == '' ? '0.00 km' : `${formatKm(r.kms)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
variant="green"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${formatKm(r.cumulativekms)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
variant="blue"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${formatKm(r.previouskms || (cancelled ? 0 : r.kms))} km`}
|
||||
tooltip="Rider KMS"
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'charges',
|
||||
accessorKey: 'deliverycharges',
|
||||
size: 150,
|
||||
minSize: 130,
|
||||
meta: { label: 'Charges', align: 'center' },
|
||||
header: () => <SectionLabel>Charges</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const cancelled = r.orderstatus === 'cancelled';
|
||||
return (
|
||||
<VStack gap={1} padding={0} vAlign="center">
|
||||
<MetricPill
|
||||
variant="red"
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={cancelled || r.deliverycharges == '' ? `0.00` : `${r.deliverycharges}.00`}
|
||||
tooltip="Delivery Charge"
|
||||
/>
|
||||
<MetricPill
|
||||
variant="green"
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={r.deliveryamt == '' ? `0.00` : `${r.deliveryamt}.00`}
|
||||
tooltip="Delivery Amount"
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
}
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[pagination.pageIndex, pagination.pageSize]
|
||||
);
|
||||
|
||||
const table = useTable({
|
||||
features: ORDERS_DETAILS_TABLE_FEATURES,
|
||||
// rows is already useMemo'd above (stable reference) — required for
|
||||
// pagination clicks to stick, see riders.js's fix for the same bug.
|
||||
data: rows,
|
||||
columns,
|
||||
getRowId: (row, index) => String(row.deliveryid ?? `${row.orderid}-${index}`),
|
||||
columnResizeMode: 'onChange',
|
||||
state: { sorting, pagination, columnSizing },
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
onColumnSizingChange: setColumnSizing
|
||||
});
|
||||
|
||||
const colCount = columns.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(fetchDeliveriesIsLoading ||
|
||||
@@ -470,7 +839,7 @@ export default function OrdersDetails() {
|
||||
Export. They used to be split across the header and a separate filter
|
||||
box below, which is why Export kept wrapping onto its own line and
|
||||
left that box half empty. */}
|
||||
<PageShell>
|
||||
<PageShell fill>
|
||||
{/* Only the primary action lives in the header. The zone picker and
|
||||
date range were up here too and the zone picker OVERLAPPED the date
|
||||
button — LocationAutocomplete brings its own width and ignores the
|
||||
@@ -481,17 +850,7 @@ export default function OrdersDetails() {
|
||||
title="Orders Details"
|
||||
subtitle={`Live · ${locaName || 'All Zones'} · ${datestatus}`}
|
||||
live
|
||||
action={
|
||||
<Button
|
||||
label="Export"
|
||||
variant="primary"
|
||||
icon={<MdFileDownload size={16} />}
|
||||
onClick={() => {
|
||||
setReportDialog(true);
|
||||
setTimeout(() => setRowsPerPage(''), 0);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
action={<Button label="Export" variant="primary" icon={<MdFileDownload size={16} />} onClick={() => setReportDialog(true)} />}
|
||||
/>
|
||||
|
||||
<KpiGrid>
|
||||
@@ -512,13 +871,14 @@ export default function OrdersDetails() {
|
||||
|
||||
{/* Toolbar: Zone · Tenant · Location · Rider · Date · Search. */}
|
||||
<DataCard
|
||||
fill
|
||||
toolbar={
|
||||
<FilterToolbar max={5} minWidth={200}>
|
||||
<LocationAutocomplete
|
||||
locaName={locaName}
|
||||
setAppId={setAppId}
|
||||
setLocoName={setLocoName}
|
||||
setPage={setPage}
|
||||
setPage={() => setPagination((p) => ({ ...p, pageIndex: 0 }))}
|
||||
placeholder="Select Zone"
|
||||
/>
|
||||
|
||||
@@ -639,19 +999,23 @@ export default function OrdersDetails() {
|
||||
icon={<MdAssignment size={28} />}
|
||||
/>
|
||||
) : (
|
||||
rows?.map((row, index) => {
|
||||
// Same paginated + sorted row model the desktop table
|
||||
// reads, so mobile and desktop never disagree on which
|
||||
// page is showing for the same filters.
|
||||
table.getRowModel().rows.map((tableRow, index) => {
|
||||
const row = tableRow.original;
|
||||
const rowStatusMeta = getStatusMeta(row.orderstatus);
|
||||
const isDelivered = row.orderstatus === 'delivered';
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.deliveryid || `${row.orderid}-${index}`}
|
||||
key={tableRow.id}
|
||||
accent={rowStatusMeta.color}
|
||||
header={
|
||||
<VStack gap={1.5} padding={0}>
|
||||
<HStack justify="between" vAlign="start" gap={1}>
|
||||
<HStack gap={1.5} vAlign="center" minWidth={0}>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
|
||||
{String(pagination.pageIndex * pagination.pageSize + index + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdGroups size={16} />
|
||||
@@ -802,248 +1166,168 @@ export default function OrdersDetails() {
|
||||
)}
|
||||
</MobileCardList>
|
||||
) : (
|
||||
<TableScroll minWidth={1600}>
|
||||
<Table density="balanced" dividers="rows" hasHover>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell style={{ minWidth: 40, width: 40, maxWidth: 40 }}>#</TableHeaderCell>
|
||||
<TableHeaderCell>Map</TableHeaderCell>
|
||||
<TableHeaderCell>Client</TableHeaderCell>
|
||||
<TableHeaderCell>Pickup</TableHeaderCell>
|
||||
<TableHeaderCell>Drop</TableHeaderCell>
|
||||
<TableHeaderCell>Status / Rider</TableHeaderCell>
|
||||
<TableHeaderCell>Assigned</TableHeaderCell>
|
||||
<TableHeaderCell>Accepted</TableHeaderCell>
|
||||
<TableHeaderCell>Arrived</TableHeaderCell>
|
||||
<TableHeaderCell>Picked</TableHeaderCell>
|
||||
<TableHeaderCell>Active</TableHeaderCell>
|
||||
<TableHeaderCell>Delivered</TableHeaderCell>
|
||||
<TableHeaderCell>Cancelled</TableHeaderCell>
|
||||
<TableHeaderCell>Notes</TableHeaderCell>
|
||||
<TableHeaderCell>KMS</TableHeaderCell>
|
||||
<TableHeaderCell>Charges</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{fetchDeliveriesIsLoading ? (
|
||||
<TableLoader rows={16} columns={16} />
|
||||
) : rows?.length == 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={16}>
|
||||
<EmptyState
|
||||
title="No orders to show"
|
||||
description={searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
|
||||
icon={<MdAssignment size={28} />}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows?.map((row, index) => {
|
||||
const isDelivered = row.orderstatus === 'delivered';
|
||||
const cancelled = row.orderstatus === 'cancelled';
|
||||
return (
|
||||
<TableRow key={row.deliveryid || `${row.orderid}-${index}`}>
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
{String(page * rowsPerPage + index + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<IconButton
|
||||
label={isDelivered ? 'View rider route' : 'Available for delivered orders'}
|
||||
tooltip={isDelivered ? 'View rider route' : 'Available for delivered orders'}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdMap size={14} />}
|
||||
isDisabled={!isDelivered}
|
||||
onClick={() => {
|
||||
if (isDelivered) {
|
||||
getdeliverylogs(row.deliveryid);
|
||||
setMapTenant(row);
|
||||
}
|
||||
<VStack gap={0} padding={0} style={{ flex: 1, minHeight: 0 }}>
|
||||
<TableScroll minWidth={table.getTotalSize()} maxHeight="100%">
|
||||
{/* table-layout: fixed makes the declared column widths
|
||||
authoritative — same reasoning as orders.js's/riders.js's/
|
||||
Tenants.js's tables. Header and body are both rendered
|
||||
FROM the column model, so they cannot drift apart. */}
|
||||
<Table
|
||||
density="balanced"
|
||||
dividers="rows"
|
||||
hasHover
|
||||
verticalAlign="middle"
|
||||
aria-label="Orders Details"
|
||||
style={{ tableLayout: 'fixed', width: '100%', minWidth: table.getTotalSize() }}
|
||||
>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} isHeaderRow>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const align = header.column.columnDef.meta?.align;
|
||||
const canSort = header.column.getCanSort();
|
||||
const sorted = header.column.getIsSorted();
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
scope="col"
|
||||
aria-sort={sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : canSort ? 'none' : undefined}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
minWidth: header.column.columnDef.minSize,
|
||||
maxWidth: header.column.columnDef.maxSize || header.getSize(),
|
||||
textAlign: align,
|
||||
position: 'relative'
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<HStack gap={1.5} vAlign="start">
|
||||
<AccentAvatar color={BRAND} size={32}>
|
||||
<MdGroups size={16} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{row.tenantname}
|
||||
</Text>
|
||||
<Tooltip content="Order ID">
|
||||
<Text type="supporting" color="secondary" weight="bold">
|
||||
#{row.orderid}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Text type="supporting" color="secondary">
|
||||
{parseDoormileTimestamp(row.deliverydate).format('DD/MM/YYYY · hh:mm A')}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{row.pickupcustomer || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.pickupcontactno}
|
||||
</Text>
|
||||
<Tooltip content={row.Pickupaddress || row.pickupsuburb || ''}>
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ cursor: 'pointer' }}>
|
||||
{row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
{row.applocation && <Badge variant="green" icon={<MdPlace size={11} />} label={row.applocation} />}
|
||||
</VStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{row.deliverycustomer || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.deliverycontactno}
|
||||
</Text>
|
||||
<Tooltip content={row.deliveryaddress || ''}>
|
||||
<Text type="supporting" color="secondary" maxLines={1} style={{ cursor: 'pointer' }}>
|
||||
{row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</VStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<VStack gap={1} padding={0}>
|
||||
<StatusBadge status={row.orderstatus} />
|
||||
{row.ridername && (
|
||||
<HStack gap={1} vAlign="center">
|
||||
<MdDirectionsBike size={12} color={DT.textMuted} />
|
||||
<Tooltip content={`Rider ID: ${row.userid}`}>
|
||||
<Text weight="bold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{row.ridername}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<StampCell value={row.assigntime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<StampCell value={row.acceptedtime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<StampCell value={row.arrivaltime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
{/* 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 muted dash. 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 style={{ verticalAlign: 'top' }}>
|
||||
<StampCell value={row.starttime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<StampCell value={row.deliverytime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
<TableCell style={{ verticalAlign: 'top' }}>
|
||||
<StampCell value={row.canceltime} formatDate={formatDate} formatTime={formatTime} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top', maxWidth: 180 }}>
|
||||
{row.ordernotes ? (
|
||||
<HStack gap={1} vAlign="start">
|
||||
<MdNoteAlt size={12} color={DT.textMuted} style={{ marginTop: 2, flexShrink: 0 }} />
|
||||
<Text type="supporting" color="secondary" maxLines={2}>
|
||||
{row.ordernotes}
|
||||
</Text>
|
||||
>
|
||||
<HStack
|
||||
gap={1}
|
||||
vAlign="center"
|
||||
justify={align === 'right' ? 'end' : align === 'center' ? 'center' : 'start'}
|
||||
role={canSort ? 'button' : undefined}
|
||||
tabIndex={canSort ? 0 : undefined}
|
||||
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||||
onKeyDown={
|
||||
canSort
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
header.column.toggleSorting();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{canSort &&
|
||||
(sorted === 'asc' ? (
|
||||
<MdArrowUpward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
|
||||
) : sorted === 'desc' ? (
|
||||
<MdArrowDownward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
|
||||
) : (
|
||||
<MdUnfoldMore size={12} color={DT.textMuted} style={{ flexShrink: 0, opacity: 0.6 }} />
|
||||
))}
|
||||
</HStack>
|
||||
) : (
|
||||
<Text type="supporting" color="secondary">
|
||||
—
|
||||
</Text>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top', textAlign: 'center' }}>
|
||||
<VStack gap={1} padding={0} vAlign="center">
|
||||
<MetricPill
|
||||
variant="red"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
variant="green"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${formatKm(row.cumulativekms)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
variant="blue"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`}
|
||||
tooltip="Rider KMS"
|
||||
/>
|
||||
</VStack>
|
||||
</TableCell>
|
||||
{header.column.getCanResize() && (
|
||||
<Center
|
||||
aria-hidden="true"
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
height: '100%',
|
||||
width: 10,
|
||||
cursor: 'col-resize',
|
||||
userSelect: 'none',
|
||||
touchAction: 'none'
|
||||
}}
|
||||
>
|
||||
<Center
|
||||
style={{
|
||||
width: 2,
|
||||
height: '60%',
|
||||
borderRadius: 'var(--radius-element)',
|
||||
backgroundColor: header.column.getIsResizing() ? DT.brand : DT.borderSubtle,
|
||||
transition: 'background-color 120ms ease'
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableCell style={{ verticalAlign: 'top', textAlign: 'center' }}>
|
||||
<VStack gap={1} padding={0} vAlign="center">
|
||||
<MetricPill
|
||||
variant="red"
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`}
|
||||
tooltip="Delivery Charge"
|
||||
/>
|
||||
<MetricPill
|
||||
variant="green"
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`}
|
||||
tooltip="Delivery Amount"
|
||||
/>
|
||||
</VStack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
<TableBody>
|
||||
{fetchDeliveriesIsLoading ? (
|
||||
<TableLoader rows={16} columns={colCount} />
|
||||
) : rows?.length == 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={colCount}>
|
||||
<EmptyState
|
||||
title="No orders to show"
|
||||
description={searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
|
||||
icon={<MdAssignment size={28} />}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
table.getRowModel().rows.map((tableRow) => {
|
||||
return (
|
||||
<TableRow key={tableRow.id}>
|
||||
{tableRow.getAllCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
verticalAlign: 'top',
|
||||
textAlign: cell.column.columnDef.meta?.align,
|
||||
maxWidth: cell.column.columnDef.maxSize || cell.column.getSize()
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{rows?.length !== 0 && (
|
||||
{/* Pagination replaces the old scroll-to-reveal sentinel — the
|
||||
unconditional drain effect above already pulls every matching
|
||||
order into `allRows` regardless of scroll position, so this
|
||||
was always paginating an in-memory array, just without any UI
|
||||
for it. */}
|
||||
{rows?.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<VStack vAlign="center" padding={3} ref={loadMoreRef}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
) : (
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
No more orders
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap" paddingInline={4} paddingBlock={2}>
|
||||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||||
{`${pagination.pageIndex * pagination.pageSize + 1}–${Math.min(
|
||||
(pagination.pageIndex + 1) * pagination.pageSize,
|
||||
rows.length
|
||||
)} of ${rows.length} order${rows.length === 1 ? '' : 's'}`}
|
||||
</Text>
|
||||
<Pagination
|
||||
page={pagination.pageIndex + 1}
|
||||
onChange={(p) => table.setPageIndex(p - 1)}
|
||||
totalItems={rows.length}
|
||||
pageSize={pagination.pageSize}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
onPageSizeChange={(size) => table.setPageSize(size)}
|
||||
size="sm"
|
||||
label="Orders details pagination"
|
||||
/>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -19,12 +19,34 @@ import {
|
||||
MdOutlineLocalShipping,
|
||||
MdOutlinePendingActions,
|
||||
MdOutlineCheckCircle,
|
||||
MdOutlineCurrencyRupee
|
||||
MdOutlineCurrencyRupee,
|
||||
MdArrowUpward,
|
||||
MdArrowDownward,
|
||||
MdUnfoldMore
|
||||
} from 'react-icons/md';
|
||||
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
|
||||
// TanStack Table v9 — same engine orders.js/riders.js/Tenants.js use. This
|
||||
// table's two-row grouped header (Orders/Deliveries each spanning 3 leaf
|
||||
// columns) uses TanStack's native column-grouping (`columns: [...]` nested
|
||||
// under a parent column) — `header.rowSpan`/`header.colSpan` are computed
|
||||
// automatically from the group depth, so the header render loop below skips
|
||||
// any header reporting `rowSpan === 0` (its cell was already merged into a
|
||||
// taller cell above it) and renders every other header with its own
|
||||
// rowSpan/colSpan, per table-core's documented placeholder-chain contract.
|
||||
import {
|
||||
useTable,
|
||||
flexRender,
|
||||
tableFeatures,
|
||||
rowSortingFeature,
|
||||
createSortedRowModel,
|
||||
sortFns,
|
||||
rowPaginationFeature,
|
||||
createPaginatedRowModel
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
@@ -36,6 +58,7 @@ import { Badge } from '@astryxdesign/core/Badge';
|
||||
import { Divider } from '@astryxdesign/core/Divider';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Pagination } from '@astryxdesign/core/Pagination';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||||
|
||||
import { getreportlocationsummary, getreportsummary, gettenantlocations, getTenants } from 'pages/api/api';
|
||||
@@ -124,6 +147,31 @@ function formatNumberToRupees(value) {
|
||||
}).format(Number(value) || 0);
|
||||
}
|
||||
|
||||
// TanStack Table v9 registers features explicitly. Sorting + pagination only
|
||||
// here — deliberately NO columnSizingFeature/columnResizingFeature: this
|
||||
// table's 2-row grouped header (Orders/Deliveries each colSpan={3}) makes a
|
||||
// per-column resize handle ambiguous (which row does the handle belong to?
|
||||
// what does resizing a group header even mean?), a materially fiddlier UX
|
||||
// problem than the other converted pages. Sorting/pagination carry the same
|
||||
// value without that complication.
|
||||
const ORDERS_SUMMARY_TABLE_FEATURES = tableFeatures({
|
||||
rowSortingFeature,
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
sortFns,
|
||||
rowPaginationFeature,
|
||||
paginatedRowModel: createPaginatedRowModel()
|
||||
});
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
||||
|
||||
// Uppercase micro-label for the table's column headings — same convention
|
||||
// orders.js's/riders.js's/Tenants.js's tables use.
|
||||
const SectionLabel = ({ children }) => (
|
||||
<Text type="label" weight="bold" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Orders Summary
|
||||
// ============================================================================
|
||||
@@ -144,6 +192,9 @@ export default function OrdersReport() {
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
const [, setPage] = useState(0);
|
||||
// Table sorting/pagination state — mirrors orders.js's/riders.js's slices.
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
|
||||
|
||||
// Clear nested filters when scope widens.
|
||||
useEffect(() => {
|
||||
@@ -360,6 +411,242 @@ export default function OrdersReport() {
|
||||
</Card>
|
||||
);
|
||||
|
||||
// A zone/tenant/location/date/search change re-scopes the list, so page 1
|
||||
// is the only page that still means anything — same reset orders.js/
|
||||
// riders.js do on their own tables.
|
||||
useEffect(() => {
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}, [appId, tenantid, locationid, startdate, enddate, debouncedSearch]);
|
||||
|
||||
const ORDERS_GROUP_BG = tint('#ef4444');
|
||||
const DELIVERIES_GROUP_BG = tint('#10b981');
|
||||
|
||||
// The column model. Orders/Deliveries are TanStack column GROUPS (a
|
||||
// `columns:` array nested under a parent column def) — this is what
|
||||
// produces the two-row header: `table.getHeaderGroups()` returns one row
|
||||
// per depth level, and TanStack computes each header's rowSpan/colSpan
|
||||
// from the group structure automatically (see the import comment above for
|
||||
// the placeholder-chain contract the render loop follows).
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'serial',
|
||||
size: 44,
|
||||
minSize: 36,
|
||||
enableSorting: false,
|
||||
meta: { label: '#' },
|
||||
header: () => <SectionLabel>#</SectionLabel>,
|
||||
cell: ({ table: t, row }) => (
|
||||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||||
{String(pagination.pageIndex * pagination.pageSize + t.getRowModel().rows.indexOf(row) + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'group',
|
||||
accessorFn: (row) => (isLocationGroup ? row.locationname : row.tenantname) || '—',
|
||||
size: 220,
|
||||
minSize: 180,
|
||||
meta: { label: isLocationGroup ? 'Location' : 'Tenant' },
|
||||
header: () => <SectionLabel>{isLocationGroup ? 'Location' : 'Tenant'}</SectionLabel>,
|
||||
cell: ({ getValue, row }) => (
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={isLocationGroup ? '#0ea5e9' : BRAND} size={36}>
|
||||
{isLocationGroup ? <MdLocationOn size={18} /> : <MdStore size={18} />}
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{getValue()}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{isLocationGroup ? row.original.locationid : row.original.tenantid}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
accessorKey: 'totalorders',
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
meta: { label: 'All', align: 'center' },
|
||||
header: () => <SectionLabel>All</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'ordersGroup',
|
||||
header: () => <SectionLabel>Orders</SectionLabel>,
|
||||
meta: { headerBg: ORDERS_GROUP_BG, headerColor: '#ef4444' },
|
||||
columns: [
|
||||
{
|
||||
id: 'orderspending',
|
||||
accessorKey: 'Orderspending',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Orders · Pending', align: 'center', headerBg: ORDERS_GROUP_BG, headerColor: '#f59e0b' },
|
||||
header: () => <SectionLabel>Pending</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'orderscancelled',
|
||||
accessorKey: 'orderscancelled',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Orders · Cancelled', align: 'center', headerBg: ORDERS_GROUP_BG, headerColor: '#ef4444' },
|
||||
header: () => <SectionLabel>Cancelled</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'orderscompleted',
|
||||
accessorKey: 'orderscompleted',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Orders · Completed', align: 'center', headerBg: ORDERS_GROUP_BG, headerColor: '#10b981' },
|
||||
header: () => <SectionLabel>Completed</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'deliveriesGroup',
|
||||
header: () => <SectionLabel>Deliveries</SectionLabel>,
|
||||
meta: { headerBg: DELIVERIES_GROUP_BG, headerColor: '#10b981' },
|
||||
columns: [
|
||||
{
|
||||
id: 'deliveriespending',
|
||||
accessorKey: 'deliveriespending',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Deliveries · Pending', align: 'center', headerBg: DELIVERIES_GROUP_BG, headerColor: '#f59e0b' },
|
||||
header: () => <SectionLabel>Pending</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'deliveriescancelled',
|
||||
accessorKey: 'deliveriescancelled',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Deliveries · Cancelled', align: 'center', headerBg: DELIVERIES_GROUP_BG, headerColor: '#ef4444' },
|
||||
header: () => <SectionLabel>Cancelled</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'deliveriescompleted',
|
||||
accessorKey: 'deliveriescompleted',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Deliveries · Completed', align: 'center', headerBg: DELIVERIES_GROUP_BG, headerColor: '#10b981' },
|
||||
header: () => <SectionLabel>Completed</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'collection',
|
||||
accessorKey: 'collectionamt',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Collection', align: 'center' },
|
||||
header: () => <SectionLabel>Collection</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<MetricPill
|
||||
color="#f59e0b"
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={Number(getValue() || 0).toFixed(2)}
|
||||
tooltip="Collection Amount"
|
||||
minWidth={90}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'kmsActual',
|
||||
accessorKey: 'kms',
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: { label: 'KMS / Actual', align: 'center' },
|
||||
header: () => <SectionLabel>KMS / Actual</SectionLabel>,
|
||||
cell: ({ row }) => (
|
||||
<VStack gap={1} padding={0} vAlign="center">
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.original.kms || 0).toFixed(2)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
<MetricPill
|
||||
color="#10b981"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.original.cumulativekms || 0).toFixed(2)} km`}
|
||||
tooltip="Actual KMS"
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
accessorFn: (row) => Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0),
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: { label: 'Amount', align: 'center' },
|
||||
header: () => <SectionLabel>Amount</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={formatNumberToRupees(getValue()).replace('₹', '').trim()}
|
||||
tooltip="Total Amount"
|
||||
minWidth={100}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
size: 90,
|
||||
minSize: 70,
|
||||
enableSorting: false,
|
||||
meta: { label: 'Action' },
|
||||
header: () => <SectionLabel>Action</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const rowKey = isLocationGroup ? r.locationname : r.tenantname;
|
||||
const isOpen = openRow === rowKey;
|
||||
return (
|
||||
<HStack justify="end">
|
||||
<IconButton
|
||||
label={isOpen ? 'Hide rider breakdown' : 'View rider breakdown'}
|
||||
tooltip={isOpen ? 'Hide rider breakdown' : 'View rider breakdown'}
|
||||
size="sm"
|
||||
variant={isOpen ? 'primary' : 'ghost'}
|
||||
icon={isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
|
||||
onClick={() => toggleBreakdown(r, rowKey)}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
}
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isLocationGroup, openRow, pagination.pageIndex, pagination.pageSize]
|
||||
);
|
||||
|
||||
const table = useTable({
|
||||
features: ORDERS_SUMMARY_TABLE_FEATURES,
|
||||
// filteredRows is already useMemo'd above (stable reference) — required
|
||||
// for pagination clicks to stick, see riders.js's fix for the same bug.
|
||||
data: filteredRows,
|
||||
columns,
|
||||
getRowId: (row, index) => String((isLocationGroup ? row.locationname : row.tenantname) ?? `row-${index}`),
|
||||
state: { sorting, pagination },
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination
|
||||
});
|
||||
|
||||
// Leaf-column count (not top-level `columns.length`, which mixes 7 leaves
|
||||
// with 2 group parents) — this is what loading/empty-state colSpans need
|
||||
// to actually span every rendered data column.
|
||||
const colCount = table.getAllLeafColumns().length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(loading || isLoadingReports) && <Loader />}
|
||||
@@ -400,9 +687,9 @@ export default function OrdersReport() {
|
||||
not a lifecycle list), so the toolbar just holds every filter
|
||||
including search — same shape as hubs.js/vehicles.js when there's
|
||||
nothing to pair search with. The two-row grouped Orders/Deliveries
|
||||
header below (rowSpan/colSpan) stays a hand-rolled table rather
|
||||
than a TanStack Table v9 column-group conversion — a materially
|
||||
larger, riskier change than this pass's chrome scope. */}
|
||||
header is a TanStack Table v9 column-group (see the `columns`
|
||||
useMemo above) — table-core computes rowSpan/colSpan from the
|
||||
group structure, the render loop below just follows it. */}
|
||||
<DataCard
|
||||
toolbar={
|
||||
<FilterToolbar max={4} minWidth={200}>
|
||||
@@ -486,7 +773,11 @@ export default function OrdersReport() {
|
||||
|
||||
{!isLoadingReports && filteredRows.length > 0 && (
|
||||
<MobileCardList scroll>
|
||||
{filteredRows.map((row, index) => {
|
||||
{/* Same paginated + sorted row model the desktop table
|
||||
reads, so mobile and desktop never disagree on which
|
||||
page is showing for the same filters. */}
|
||||
{table.getRowModel().rows.map((tableRow) => {
|
||||
const row = tableRow.original;
|
||||
const rowKey = isLocationGroup ? row.locationname : row.tenantname;
|
||||
const isOpen = openRow === rowKey;
|
||||
const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
|
||||
@@ -494,7 +785,7 @@ export default function OrdersReport() {
|
||||
|
||||
return (
|
||||
<MobileCard
|
||||
key={`${rowKey}-${index}`}
|
||||
key={tableRow.id}
|
||||
accent={rowAccent}
|
||||
selected={isOpen}
|
||||
header={
|
||||
@@ -592,59 +883,78 @@ export default function OrdersReport() {
|
||||
</>
|
||||
) : (
|
||||
<TableScroll minWidth={1200}>
|
||||
<Table density="balanced" dividers="rows" hasHover>
|
||||
<Table density="balanced" dividers="rows" hasHover style={{ width: '100%' }} aria-label="Orders Summary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell rowSpan={2}>#</TableHeaderCell>
|
||||
<TableHeaderCell rowSpan={2}>{isLocationGroup ? 'Location' : 'Tenant'}</TableHeaderCell>
|
||||
<TableHeaderCell rowSpan={2} style={{ textAlign: 'center' }}>
|
||||
All
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell colSpan={3} style={{ textAlign: 'center', backgroundColor: tint('#ef4444'), color: '#ef4444' }}>
|
||||
Orders
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell colSpan={3} style={{ textAlign: 'center', backgroundColor: tint('#10b981'), color: '#10b981' }}>
|
||||
Deliveries
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell rowSpan={2} style={{ textAlign: 'center' }}>
|
||||
Collection
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell rowSpan={2} style={{ textAlign: 'center' }}>
|
||||
KMS / Actual
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell rowSpan={2} style={{ textAlign: 'center' }}>
|
||||
Amount
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell rowSpan={2}>Action</TableHeaderCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHeaderCell style={{ textAlign: 'center', backgroundColor: tint('#ef4444'), color: '#f59e0b' }}>
|
||||
Pending
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center', backgroundColor: tint('#ef4444'), color: '#ef4444' }}>
|
||||
Cancelled
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center', backgroundColor: tint('#ef4444'), color: '#10b981' }}>
|
||||
Completed
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center', backgroundColor: tint('#10b981'), color: '#f59e0b' }}>
|
||||
Pending
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center', backgroundColor: tint('#10b981'), color: '#ef4444' }}>
|
||||
Cancelled
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center', backgroundColor: tint('#10b981'), color: '#10b981' }}>
|
||||
Completed
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} isHeaderRow>
|
||||
{headerGroup.headers.map((header) => {
|
||||
// Placeholder chain: a leaf column shallower than the
|
||||
// table's max depth gets padded with placeholders;
|
||||
// every header covering the same visual cell after
|
||||
// the first reports rowSpan 0 and must be skipped —
|
||||
// rendering it would draw an extra empty <th>.
|
||||
if (header.rowSpan === 0) return null;
|
||||
const meta = header.column.columnDef.meta;
|
||||
const align = meta?.align;
|
||||
const canSort = header.column.getCanSort();
|
||||
const sorted = header.column.getIsSorted();
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
scope="col"
|
||||
rowSpan={header.rowSpan}
|
||||
colSpan={header.colSpan}
|
||||
aria-sort={sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : canSort ? 'none' : undefined}
|
||||
style={{
|
||||
textAlign: align,
|
||||
backgroundColor: meta?.headerBg,
|
||||
color: meta?.headerColor
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<HStack
|
||||
gap={1}
|
||||
vAlign="center"
|
||||
justify={align === 'center' ? 'center' : 'start'}
|
||||
role={canSort ? 'button' : undefined}
|
||||
tabIndex={canSort ? 0 : undefined}
|
||||
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||||
onKeyDown={
|
||||
canSort
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
header.column.toggleSorting();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{canSort &&
|
||||
(sorted === 'asc' ? (
|
||||
<MdArrowUpward size={12} style={{ flexShrink: 0 }} />
|
||||
) : sorted === 'desc' ? (
|
||||
<MdArrowDownward size={12} style={{ flexShrink: 0 }} />
|
||||
) : (
|
||||
<MdUnfoldMore size={12} style={{ flexShrink: 0, opacity: 0.5 }} />
|
||||
))}
|
||||
</HStack>
|
||||
)}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{isLoadingReports && <TableLoader rows={5} columns={13} />}
|
||||
{isLoadingReports && <TableLoader rows={5} columns={colCount} />}
|
||||
|
||||
{!isLoadingReports && filteredRows.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={13}>
|
||||
<TableCell colSpan={colCount}>
|
||||
<EmptyState
|
||||
title={`No ${isLocationGroup ? 'locations' : 'tenants'} to show`}
|
||||
description={
|
||||
@@ -660,111 +970,24 @@ export default function OrdersReport() {
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{filteredRows.map((row, index) => {
|
||||
{table.getRowModel().rows.map((tableRow) => {
|
||||
const row = tableRow.original;
|
||||
const rowKey = isLocationGroup ? row.locationname : row.tenantname;
|
||||
const isOpen = openRow === rowKey;
|
||||
const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
|
||||
|
||||
return (
|
||||
<React.Fragment key={`${rowKey}-${index}`}>
|
||||
<React.Fragment key={tableRow.id}>
|
||||
<TableRow style={{ backgroundColor: isOpen ? tint(BRAND) : undefined }}>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={isLocationGroup ? '#0ea5e9' : BRAND} size={36}>
|
||||
{isLocationGroup ? <MdLocationOn size={18} /> : <MdStore size={18} />}
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{(isLocationGroup ? row.locationname : row.tenantname) || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{isLocationGroup ? row.locationid : row.tenantid}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.totalorders} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.Orderspending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.orderscancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.orderscompleted} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.deliveriespending} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.deliveriescancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<CountCell value={row.deliveriescompleted} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<MetricPill
|
||||
color="#f59e0b"
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={Number(row.collectionamt || 0).toFixed(2)}
|
||||
tooltip="Collection Amount"
|
||||
minWidth={90}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<VStack gap={1} padding={0} vAlign="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"
|
||||
/>
|
||||
</VStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={formatNumberToRupees(amount).replace('₹', '').trim()}
|
||||
tooltip="Total Amount"
|
||||
minWidth={100}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<HStack justify="end">
|
||||
<IconButton
|
||||
label={isOpen ? 'Hide rider breakdown' : 'View rider breakdown'}
|
||||
tooltip={isOpen ? 'Hide rider breakdown' : 'View rider breakdown'}
|
||||
size="sm"
|
||||
variant={isOpen ? 'primary' : 'ghost'}
|
||||
icon={isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
|
||||
onClick={() => toggleBreakdown(row, rowKey)}
|
||||
/>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
{tableRow.getAllCells().map((cell) => (
|
||||
<TableCell key={cell.id} style={{ textAlign: cell.column.columnDef.meta?.align }}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{isOpen && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={13} style={{ padding: 0, backgroundColor: tint(BRAND) }}>
|
||||
<TableCell colSpan={colCount} style={{ padding: 0, backgroundColor: tint(BRAND) }}>
|
||||
<div style={{ padding: 12 }}>
|
||||
<RiderBreakdown label={isLocationGroup ? row.locationname : row.tenantname} />
|
||||
</div>
|
||||
@@ -779,8 +1002,31 @@ export default function OrdersReport() {
|
||||
</TableScroll>
|
||||
)}
|
||||
|
||||
{/* Pagination sits above the Grand Total bar — the total is
|
||||
deliberately computed from the FULL filtered set (`stats`,
|
||||
above), not just the current page, so it keeps summing every
|
||||
matching row regardless of which page is showing. */}
|
||||
{filteredRows.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap" paddingInline={4} paddingBlock={2}>
|
||||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||||
{`${pagination.pageIndex * pagination.pageSize + 1}–${Math.min(
|
||||
(pagination.pageIndex + 1) * pagination.pageSize,
|
||||
filteredRows.length
|
||||
)} of ${filteredRows.length} ${isLocationGroup ? 'location' : 'tenant'}${filteredRows.length === 1 ? '' : 's'}`}
|
||||
</Text>
|
||||
<Pagination
|
||||
page={pagination.pageIndex + 1}
|
||||
onChange={(p) => table.setPageIndex(p - 1)}
|
||||
totalItems={filteredRows.length}
|
||||
pageSize={pagination.pageSize}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
onPageSizeChange={(size) => table.setPageSize(size)}
|
||||
size="sm"
|
||||
label="Orders summary pagination"
|
||||
/>
|
||||
</HStack>
|
||||
<Divider />
|
||||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap" padding={2} style={{ background: '#ffffff' }}>
|
||||
<HStack gap={2} vAlign="center" wrap="wrap">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
@@ -17,15 +17,34 @@ import {
|
||||
MdPerson,
|
||||
MdOutlineLocalShipping,
|
||||
MdOutlineCheckCircle,
|
||||
MdOutlineCurrencyRupee
|
||||
MdOutlineCurrencyRupee,
|
||||
MdArrowUpward,
|
||||
MdArrowDownward,
|
||||
MdUnfoldMore
|
||||
} from 'react-icons/md';
|
||||
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
dayjs.extend(utc);
|
||||
|
||||
// TanStack Table v9 — same engine orders.js/riders.js/Tenants.js use, so
|
||||
// sorting/resizing/pagination behave identically across the console.
|
||||
import {
|
||||
useTable,
|
||||
flexRender,
|
||||
tableFeatures,
|
||||
rowSortingFeature,
|
||||
createSortedRowModel,
|
||||
sortFns,
|
||||
columnSizingFeature,
|
||||
columnResizingFeature,
|
||||
rowPaginationFeature,
|
||||
createPaginatedRowModel
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { Center } from '@astryxdesign/core/Center';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||||
@@ -33,6 +52,7 @@ import { Tooltip } from '@astryxdesign/core/Tooltip';
|
||||
import { Divider } from '@astryxdesign/core/Divider';
|
||||
import { Dialog } from '@astryxdesign/core/Dialog';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Pagination } from '@astryxdesign/core/Pagination';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||||
|
||||
import { fetchRidersSummary } from 'pages/api/api';
|
||||
@@ -121,6 +141,29 @@ function formatNumberToRupees(value) {
|
||||
}).format(Number(value) || 0);
|
||||
}
|
||||
|
||||
// TanStack Table v9 registers features explicitly — only sorting/resizing/
|
||||
// pagination ship here (no row-selection or column-visibility, since this
|
||||
// table has no bulk-select or column-hide flow).
|
||||
const RIDERS_SUMMARY_TABLE_FEATURES = tableFeatures({
|
||||
rowSortingFeature,
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
sortFns,
|
||||
columnSizingFeature,
|
||||
columnResizingFeature,
|
||||
rowPaginationFeature,
|
||||
paginatedRowModel: createPaginatedRowModel()
|
||||
});
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
||||
|
||||
// Uppercase micro-label for the table's column headings — same convention
|
||||
// orders.js's/riders.js's/Tenants.js's tables use.
|
||||
const SectionLabel = ({ children }) => (
|
||||
<Text type="label" weight="bold" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// ==============================|| Riders Summary ||============================== //
|
||||
|
||||
export default function RidersSummary() {
|
||||
@@ -141,6 +184,11 @@ export default function RidersSummary() {
|
||||
const [routeLoading, setRouteLoading] = useState(false);
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
// Table sorting/resizing/pagination state — mirrors orders.js's/riders.js's
|
||||
// slices.
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
|
||||
const [columnSizing, setColumnSizing] = useState({});
|
||||
|
||||
// ==============================|| fetchRidersSummary ||============================== //
|
||||
const { isLoading: isLoadingReports, data: rows } = useQuery({
|
||||
@@ -162,6 +210,12 @@ export default function RidersSummary() {
|
||||
return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q)));
|
||||
}, [rows, debouncedSearch]);
|
||||
|
||||
// A zone/date/search change re-scopes the list, so page 1 is the only page
|
||||
// that still means anything — same reset orders.js/riders.js do.
|
||||
useEffect(() => {
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}, [appId, startdate, enddate, debouncedSearch]);
|
||||
|
||||
// Aggregate KPIs from the loaded summary.
|
||||
const stats = useMemo(() => {
|
||||
if (!rows || rows.length === 0) return { riders: 0, orders: 0, delivered: 0, amount: 0 };
|
||||
@@ -287,6 +341,169 @@ export default function RidersSummary() {
|
||||
</Card>
|
||||
);
|
||||
|
||||
// The column model — everything a column IS declared once here instead of
|
||||
// a hardcoded header row + hand-written body row, same shape as
|
||||
// orders.js's/riders.js's/Tenants.js's `columns`. Row-expand state
|
||||
// (openRow) was already keyed by `row.userid`, not position, so it stays
|
||||
// correct once sorting/pagination reorder rows — no change needed there.
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'serial',
|
||||
size: 44,
|
||||
minSize: 36,
|
||||
maxSize: 52,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
meta: { label: '#' },
|
||||
header: () => <SectionLabel>#</SectionLabel>,
|
||||
cell: ({ table: t, row }) => (
|
||||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||||
{String(pagination.pageIndex * pagination.pageSize + t.getRowModel().rows.indexOf(row) + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'rider',
|
||||
accessorFn: (row) => row.displayname || '—',
|
||||
size: 220,
|
||||
minSize: 180,
|
||||
meta: { label: 'Rider' },
|
||||
header: () => <SectionLabel>Rider</SectionLabel>,
|
||||
cell: ({ getValue, row }) => (
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={BRAND} size={36}>
|
||||
<MdPerson size={18} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{getValue()}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{row.original.userid}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
accessorKey: 'assigned',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Orders' },
|
||||
header: () => <SectionLabel>Orders</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'accepted',
|
||||
accessorKey: 'accepted',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Accepted' },
|
||||
header: () => <SectionLabel>Accepted</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'cancelled',
|
||||
accessorKey: 'cancelled',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Cancelled' },
|
||||
header: () => <SectionLabel>Cancelled</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'delivered',
|
||||
accessorKey: 'delivered',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
meta: { label: 'Delivered' },
|
||||
header: () => <SectionLabel>Delivered</SectionLabel>,
|
||||
cell: ({ getValue }) => <CountCell value={getValue()} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
},
|
||||
{
|
||||
id: 'kms',
|
||||
accessorKey: 'riderkms',
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
meta: { label: 'Kms' },
|
||||
header: () => <SectionLabel>Kms</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<MetricPill color="#ef4444" icon={<MdStraighten size={11} />} label={`${Number(getValue() || 0).toFixed(2)} km`} tooltip="KMS" />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
accessorFn: (row) => Number(row.ridercharges) || 0,
|
||||
size: 140,
|
||||
minSize: 120,
|
||||
meta: { label: 'Amount' },
|
||||
header: () => <SectionLabel>Amount</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={formatNumberToRupees(getValue()).replace('₹', '').trim()}
|
||||
tooltip="Total Amount"
|
||||
minWidth={100}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
size: 110,
|
||||
minSize: 90,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
meta: { label: 'Action' },
|
||||
header: () => <SectionLabel>Action</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const isOpen = openRow === r.userid;
|
||||
return (
|
||||
<HStack gap={0.75} justify="end">
|
||||
<IconButton
|
||||
label="View planned route"
|
||||
tooltip="View planned route"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdMap size={14} />}
|
||||
onClick={() => openRoutePreview(r)}
|
||||
/>
|
||||
<IconButton
|
||||
label={isOpen ? 'Collapse breakdown' : 'Expand breakdown'}
|
||||
tooltip={isOpen ? 'Collapse breakdown' : 'Expand breakdown'}
|
||||
size="sm"
|
||||
variant={isOpen ? 'primary' : 'ghost'}
|
||||
icon={isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
|
||||
onClick={() => toggleBreakdown(r)}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
}
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[pagination.pageIndex, pagination.pageSize, openRow]
|
||||
);
|
||||
|
||||
const table = useTable({
|
||||
features: RIDERS_SUMMARY_TABLE_FEATURES,
|
||||
// filteredRows is already useMemo'd above (stable reference) — required
|
||||
// for pagination clicks to stick, see riders.js's fix for the same bug.
|
||||
data: filteredRows,
|
||||
columns,
|
||||
getRowId: (row, index) => String(row.userid ?? `row-${index}`),
|
||||
columnResizeMode: 'onChange',
|
||||
state: { sorting, pagination, columnSizing },
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
onColumnSizingChange: setColumnSizing
|
||||
});
|
||||
|
||||
const colCount = columns.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoadingReports || loading) && <Loader />}
|
||||
@@ -350,13 +567,17 @@ export default function RidersSummary() {
|
||||
icon={<MdDirectionsBike size={28} />}
|
||||
/>
|
||||
) : (
|
||||
filteredRows.map((row, index) => {
|
||||
// Same paginated + sorted row model the desktop table reads,
|
||||
// so mobile and desktop never disagree on what page is
|
||||
// showing for the same filters.
|
||||
table.getRowModel().rows.map((tableRow, index) => {
|
||||
const row = tableRow.original;
|
||||
const isOpen = openRow === row.userid;
|
||||
const amount = Number(row.ridercharges) || 0;
|
||||
const riderName = row?.displayname || '—';
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.userid || index}
|
||||
key={tableRow.id}
|
||||
accent={BRAND}
|
||||
selected={isOpen}
|
||||
header={
|
||||
@@ -370,7 +591,7 @@ export default function RidersSummary() {
|
||||
{riderName}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
#{String(index + 1).padStart(2, '0')} · ID #{row.userid}
|
||||
#{String(pagination.pageIndex * pagination.pageSize + index + 1).padStart(2, '0')} · ID #{row.userid}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
@@ -434,27 +655,108 @@ export default function RidersSummary() {
|
||||
)}
|
||||
</MobileCardList>
|
||||
) : (
|
||||
<TableScroll minWidth={1080}>
|
||||
<Table density="balanced" dividers="rows" hasHover>
|
||||
<TableScroll minWidth={table.getTotalSize()}>
|
||||
{/* table-layout: fixed makes the declared column widths
|
||||
authoritative — same reasoning as orders.js's/riders.js's/
|
||||
Tenants.js's tables. Header and body are both rendered FROM
|
||||
the column model, so they cannot drift apart. */}
|
||||
<Table
|
||||
density="balanced"
|
||||
dividers="rows"
|
||||
hasHover
|
||||
verticalAlign="middle"
|
||||
aria-label="Riders Summary"
|
||||
style={{ tableLayout: 'fixed', width: '100%', minWidth: table.getTotalSize() }}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell style={{ minWidth: 40, width: 40, maxWidth: 40 }}>#</TableHeaderCell>
|
||||
<TableHeaderCell>Rider</TableHeaderCell>
|
||||
<TableHeaderCell>Orders</TableHeaderCell>
|
||||
<TableHeaderCell>Accepted</TableHeaderCell>
|
||||
<TableHeaderCell>Cancelled</TableHeaderCell>
|
||||
<TableHeaderCell>Delivered</TableHeaderCell>
|
||||
<TableHeaderCell>KMS</TableHeaderCell>
|
||||
<TableHeaderCell>Amount</TableHeaderCell>
|
||||
<TableHeaderCell>Action</TableHeaderCell>
|
||||
</TableRow>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} isHeaderRow>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const align = header.column.columnDef.meta?.align;
|
||||
const canSort = header.column.getCanSort();
|
||||
const sorted = header.column.getIsSorted();
|
||||
return (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
scope="col"
|
||||
aria-sort={sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : canSort ? 'none' : undefined}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
minWidth: header.column.columnDef.minSize,
|
||||
maxWidth: header.column.columnDef.maxSize || header.getSize(),
|
||||
textAlign: align,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
gap={1}
|
||||
vAlign="center"
|
||||
justify={align === 'right' ? 'end' : align === 'center' ? 'center' : 'start'}
|
||||
role={canSort ? 'button' : undefined}
|
||||
tabIndex={canSort ? 0 : undefined}
|
||||
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||||
onKeyDown={
|
||||
canSort
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
header.column.toggleSorting();
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{canSort &&
|
||||
(sorted === 'asc' ? (
|
||||
<MdArrowUpward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
|
||||
) : sorted === 'desc' ? (
|
||||
<MdArrowDownward size={12} color={DT.textPrimary} style={{ flexShrink: 0 }} />
|
||||
) : (
|
||||
<MdUnfoldMore size={12} color={DT.textMuted} style={{ flexShrink: 0, opacity: 0.6 }} />
|
||||
))}
|
||||
</HStack>
|
||||
|
||||
{header.column.getCanResize() && (
|
||||
<Center
|
||||
aria-hidden="true"
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
height: '100%',
|
||||
width: 10,
|
||||
cursor: 'col-resize',
|
||||
userSelect: 'none',
|
||||
touchAction: 'none'
|
||||
}}
|
||||
>
|
||||
<Center
|
||||
style={{
|
||||
width: 2,
|
||||
height: '60%',
|
||||
borderRadius: 'var(--radius-element)',
|
||||
backgroundColor: header.column.getIsResizing() ? DT.brand : DT.borderSubtle,
|
||||
transition: 'background-color 120ms ease'
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</TableHeaderCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{isLoadingReports && <TableLoader rows={5} columns={9} />}
|
||||
{isLoadingReports && <TableLoader rows={5} columns={colCount} />}
|
||||
{(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9}>
|
||||
<TableCell colSpan={colCount}>
|
||||
<EmptyState
|
||||
title="No riders to show"
|
||||
description={searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'}
|
||||
@@ -463,91 +765,28 @@ export default function RidersSummary() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row, index) => {
|
||||
table.getRowModel().rows.map((tableRow) => {
|
||||
const row = tableRow.original;
|
||||
const isOpen = openRow === row.userid;
|
||||
const amount = Number(row.ridercharges) || 0;
|
||||
return (
|
||||
<React.Fragment key={row.userid || index}>
|
||||
<React.Fragment key={tableRow.id}>
|
||||
<TableRow style={{ backgroundColor: isOpen ? tint(BRAND) : undefined }}>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<HStack gap={2} vAlign="center">
|
||||
<AccentAvatar color={BRAND} size={36}>
|
||||
<MdPerson size={18} />
|
||||
</AccentAvatar>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{row?.displayname || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
ID #{row.userid}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<CountCell value={row.assigned} color={BRAND} icon={<MdLocalShipping size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<CountCell value={row.accepted} color="#f59e0b" icon={<MdHourglassEmpty size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<CountCell value={row.cancelled} color="#ef4444" icon={<MdCancel size={11} />} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<CountCell value={row.delivered} color="#10b981" icon={<MdCheckCircle size={11} />} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<MetricPill
|
||||
color="#ef4444"
|
||||
icon={<MdStraighten size={11} />}
|
||||
label={`${Number(row.riderkms || 0).toFixed(2)} km`}
|
||||
tooltip="KMS"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<MetricPill
|
||||
color={BRAND}
|
||||
icon={<MdCurrencyRupee size={11} />}
|
||||
label={formatNumberToRupees(amount).replace('₹', '').trim()}
|
||||
tooltip="Total Amount"
|
||||
minWidth={100}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<HStack gap={0.75} justify="end">
|
||||
<IconButton
|
||||
label="View planned route"
|
||||
tooltip="View planned route"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdMap size={14} />}
|
||||
onClick={() => openRoutePreview(row)}
|
||||
/>
|
||||
<IconButton
|
||||
label={isOpen ? 'Collapse breakdown' : 'Expand breakdown'}
|
||||
tooltip={isOpen ? 'Collapse breakdown' : 'Expand breakdown'}
|
||||
size="sm"
|
||||
variant={isOpen ? 'primary' : 'ghost'}
|
||||
icon={isOpen ? <MdExpandLess size={16} /> : <MdExpandMore size={16} />}
|
||||
onClick={() => toggleBreakdown(row)}
|
||||
/>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
{tableRow.getAllCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
textAlign: cell.column.columnDef.meta?.align,
|
||||
maxWidth: cell.column.columnDef.maxSize || cell.column.getSize()
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{isOpen && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} style={{ padding: 0, backgroundColor: tint(BRAND) }}>
|
||||
<TableCell colSpan={colCount} style={{ padding: 0, backgroundColor: tint(BRAND) }}>
|
||||
<div style={{ padding: 12 }}>
|
||||
<TenantBreakdown label={row?.displayname || ''} />
|
||||
</div>
|
||||
@@ -563,8 +802,31 @@ export default function RidersSummary() {
|
||||
</TableScroll>
|
||||
)}
|
||||
|
||||
{/* Pagination sits above the Grand Total bar — the total is
|
||||
deliberately computed from the FULL filtered set (`total`,
|
||||
above), not just the current page, so it keeps summing every
|
||||
matching rider regardless of which page is showing. */}
|
||||
{filteredRows.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap" paddingInline={4} paddingBlock={2}>
|
||||
<Text type="supporting" weight="bold" color="secondary" hasTabularNumbers>
|
||||
{`${pagination.pageIndex * pagination.pageSize + 1}–${Math.min(
|
||||
(pagination.pageIndex + 1) * pagination.pageSize,
|
||||
filteredRows.length
|
||||
)} of ${filteredRows.length} rider${filteredRows.length === 1 ? '' : 's'}`}
|
||||
</Text>
|
||||
<Pagination
|
||||
page={pagination.pageIndex + 1}
|
||||
onChange={(p) => table.setPageIndex(p - 1)}
|
||||
totalItems={filteredRows.length}
|
||||
pageSize={pagination.pageSize}
|
||||
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
||||
onPageSizeChange={(size) => table.setPageSize(size)}
|
||||
size="sm"
|
||||
label="Riders summary pagination"
|
||||
/>
|
||||
</HStack>
|
||||
<Divider />
|
||||
<HStack justify="end" vAlign="center" gap={2} padding={2} style={{ background: '#ffffff' }}>
|
||||
<Text type="label" weight="bold" color="secondary" style={{ letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
||||
|
||||
Reference in New Issue
Block a user