updatess on the ui changs
This commit is contained in:
@@ -1,9 +1,23 @@
|
||||
import { useState, useEffect, useRef, Fragment } from 'react';
|
||||
import { useState, useEffect, useMemo, Fragment } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
import dayjs from 'dayjs';
|
||||
dayjs.extend(utc);
|
||||
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
// TanStack Table v9 — same engine orders.js uses, so sorting/resizing/
|
||||
// pagination behave identically across the two pages.
|
||||
import {
|
||||
useTable,
|
||||
flexRender,
|
||||
tableFeatures,
|
||||
rowSortingFeature,
|
||||
createSortedRowModel,
|
||||
sortFns,
|
||||
columnSizingFeature,
|
||||
columnResizingFeature,
|
||||
rowPaginationFeature,
|
||||
createPaginatedRowModel
|
||||
} from '@tanstack/react-table';
|
||||
import {
|
||||
MdCheckCircle,
|
||||
MdCancel,
|
||||
@@ -14,6 +28,9 @@ import {
|
||||
MdEdit,
|
||||
MdKeyboardArrowDown,
|
||||
MdKeyboardArrowUp,
|
||||
MdArrowUpward,
|
||||
MdArrowDownward,
|
||||
MdUnfoldMore,
|
||||
MdLocationOn,
|
||||
MdBatteryStd,
|
||||
MdPowerSettingsNew,
|
||||
@@ -32,6 +49,7 @@ import { Grid } from '@astryxdesign/core/Grid';
|
||||
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 { IconButton } from '@astryxdesign/core/IconButton';
|
||||
import { Tooltip } from '@astryxdesign/core/Tooltip';
|
||||
@@ -42,8 +60,10 @@ import { TabList, Tab } from '@astryxdesign/core/TabList';
|
||||
import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl';
|
||||
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
||||
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
||||
import { Divider } from '@astryxdesign/core/Divider';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Pagination } from '@astryxdesign/core/Pagination';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||||
|
||||
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
|
||||
@@ -107,6 +127,29 @@ const KPI_META = (summary) => [
|
||||
{ key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdOutlineCancel, value: summary?.inactive ?? 0 }
|
||||
];
|
||||
|
||||
// TanStack Table v9 registers features explicitly, same pattern orders.js
|
||||
// uses — only sorting/resizing/pagination ship here (no row-selection or
|
||||
// column-visibility, since this table has no bulk-select flow).
|
||||
const RIDERS_TABLE_FEATURES = tableFeatures({
|
||||
rowSortingFeature,
|
||||
sortedRowModel: createSortedRowModel(),
|
||||
sortFns,
|
||||
columnSizingFeature,
|
||||
columnResizingFeature,
|
||||
rowPaginationFeature,
|
||||
paginatedRowModel: createPaginatedRowModel()
|
||||
});
|
||||
|
||||
const RIDERS_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
||||
|
||||
// Uppercase micro-label for the table's column headings — same convention
|
||||
// orders.js's table uses.
|
||||
const SectionLabel = ({ children }) => (
|
||||
<Text type="label" weight="bold" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// Inline stat chip used in the rider-logs collapse row. Mirrors the StatChip
|
||||
// pattern from the pricing page so the telemetry block reads at a glance.
|
||||
const LogChip = ({ color, icon: Icon, label, value }) => (
|
||||
@@ -182,15 +225,17 @@ const Riders = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsNarrowViewport();
|
||||
const loadMoreRef = useRef();
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
const [locaName, setLocoName] = useState('All');
|
||||
const [appId, setAppId] = useState(0);
|
||||
const [tabvalue, setTabvalue] = useState(0);
|
||||
const roleid = localStorage.getItem('roleid');
|
||||
const totalCols = roleid == 1 ? 11 : 10;
|
||||
const [logsRow, setLogsRow] = useState(null);
|
||||
// Table sorting/resizing/pagination state — mirrors orders.js's slices.
|
||||
const [sorting, setSorting] = useState([]);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
|
||||
const [columnSizing, setColumnSizing] = useState({});
|
||||
const [riderLogsdata, setRiderLogsdata] = useState(null);
|
||||
const [historyDate, setHistoryDate] = useState(dayjs());
|
||||
const [historyStatus, setHistoryStatus] = useState('all');
|
||||
@@ -320,62 +365,55 @@ const Riders = () => {
|
||||
};
|
||||
|
||||
// ==============================|| fetchAllRiders||============================== //
|
||||
// GET /admin/milers isn't paginated (fetchAllRiders always returns the full
|
||||
// list in one shot) — getNextPageParam must not request a "next page" or it
|
||||
// re-fetches and appends the identical full list again, duplicating every
|
||||
// rider in `rows`.
|
||||
const {
|
||||
data: allRidersData,
|
||||
isLoading: allRidersLoading,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
hasNextPage
|
||||
} = useInfiniteQuery({
|
||||
// GET /admin/milers isn't paginated — fetchAllRiders always returns the
|
||||
// full list in one shot (`{ details, nextPage: undefined }`). Previously
|
||||
// wrapped in useInfiniteQuery with a getNextPageParam that always returned
|
||||
// undefined, so "loading more" never actually fetched a second page — a
|
||||
// plain bulk fetch, same shape orders.js uses for bookings, with the table
|
||||
// below paginating the already-complete list client-side.
|
||||
const { data: allRidersResult, isLoading: allRidersLoading } = useQuery({
|
||||
queryKey: ['allriders', appId, debouncedSearch, tabvalue],
|
||||
queryFn: fetchAllRiders,
|
||||
getNextPageParam: () => undefined
|
||||
queryFn: fetchAllRiders
|
||||
});
|
||||
|
||||
const rows = (allRidersData?.pages.flatMap((page) => page.details || []) || []).filter(Boolean);
|
||||
const rows = useMemo(() => (allRidersResult?.details || []).filter(Boolean), [allRidersResult]);
|
||||
|
||||
// GET /admin/milers accepts no search/status params — debouncedSearch and
|
||||
// tabvalue only trigger a refetch (via the queryKey) that returns the exact
|
||||
// same full list every time. Search and the "Active" tab were both silently
|
||||
// no-ops until filtered client-side here.
|
||||
const filteredRows = rows.filter((row) => {
|
||||
// "Active" here means currently online/working (Available/Assigned/
|
||||
// On_Pickup) — Offline and Blocked (confirmed live enum) both mean not
|
||||
// working.
|
||||
const status = String(row.availabilitystatus || '').toLowerCase();
|
||||
if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false;
|
||||
if (!debouncedSearch) return true;
|
||||
const q = debouncedSearch.toLowerCase();
|
||||
return [row.displayname, row.phone].some((v) =>
|
||||
String(v || '')
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
);
|
||||
});
|
||||
//
|
||||
// Memoized (unlike the pre-TanStack version of this file) because the table
|
||||
// reads this as `data`: TanStack memoizes its core row model on the `data`
|
||||
// REFERENCE and auto-resets `pageIndex` to 0 whenever that reference
|
||||
// changes. An unmemoized `.filter()` returns a new array every render —
|
||||
// including the very render a page-2/3/4 click causes — so pagination
|
||||
// looked broken (every click snapped straight back to page 1). orders.js's
|
||||
// `filteredRows` was already memoized for the same reason.
|
||||
const filteredRows = useMemo(
|
||||
() =>
|
||||
rows.filter((row) => {
|
||||
// "Active" here means currently online/working (Available/Assigned/
|
||||
// On_Pickup) — Offline and Blocked (confirmed live enum) both mean
|
||||
// not working.
|
||||
const status = String(row.availabilitystatus || '').toLowerCase();
|
||||
if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false;
|
||||
if (!debouncedSearch) return true;
|
||||
const q = debouncedSearch.toLowerCase();
|
||||
return [row.displayname, row.phone].some((v) =>
|
||||
String(v || '')
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
);
|
||||
}),
|
||||
[rows, tabvalue, debouncedSearch]
|
||||
);
|
||||
|
||||
// A tab switch or search re-scopes the list, so page 1 is the only page
|
||||
// that still means anything — same reset orders.js does on its own table.
|
||||
useEffect(() => {
|
||||
if (!hasNextPage) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: document.querySelector('.dt-table-scroll'),
|
||||
rootMargin: '0px',
|
||||
threshold: 1.0
|
||||
}
|
||||
);
|
||||
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
||||
return () => {
|
||||
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
||||
};
|
||||
}, [hasNextPage, fetchNextPage]);
|
||||
setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
}, [tabvalue, debouncedSearch]);
|
||||
|
||||
// Per-row status meta — falls back to "unknown" if the rider's state key
|
||||
// isn't in the palette (e.g. brand-new status string from the backend).
|
||||
@@ -501,6 +539,222 @@ const Riders = () => {
|
||||
</HStack>
|
||||
);
|
||||
|
||||
// The column model — everything a column IS (id, accessor, header, cell,
|
||||
// width, sortability) declared once here instead of split across a
|
||||
// hardcoded header row and a hand-written body row, same shape as
|
||||
// orders.js's `columns`. The Action column is appended conditionally
|
||||
// because it only exists for roleid 1, not hidden via a visibility slice.
|
||||
const columns = useMemo(() => {
|
||||
const base = [
|
||||
{
|
||||
id: 'serial',
|
||||
size: 40,
|
||||
minSize: 32,
|
||||
maxSize: 48,
|
||||
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: 'id',
|
||||
accessorKey: 'userid',
|
||||
size: 90,
|
||||
minSize: 70,
|
||||
meta: { label: 'ID' },
|
||||
header: () => <SectionLabel>ID</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '3px 8px',
|
||||
borderRadius: 999,
|
||||
background: '#ffffff',
|
||||
border: `1px solid ${edge(BRAND)}`,
|
||||
color: BRAND,
|
||||
fontWeight: 800,
|
||||
fontSize: 11,
|
||||
minWidth: 56
|
||||
}}
|
||||
>
|
||||
#{getValue()}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'rider',
|
||||
accessorFn: (row) => row.displayname || row.authname || '—',
|
||||
size: 200,
|
||||
minSize: 160,
|
||||
meta: { label: 'Rider' },
|
||||
header: () => <SectionLabel>Rider</SectionLabel>,
|
||||
cell: ({ getValue, row }) => (
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{getValue()}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.original.phone || '—'}
|
||||
</Text>
|
||||
</VStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'address',
|
||||
accessorFn: (row) => row.suburb || row.address || '',
|
||||
size: 220,
|
||||
minSize: 160,
|
||||
meta: { label: 'Address' },
|
||||
header: () => <SectionLabel>Address</SectionLabel>,
|
||||
cell: ({ row }) => (
|
||||
<Tooltip content={row.original.address || ''}>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text type="supporting" weight="bold" color="secondary" maxLines={1}>
|
||||
{row.original.suburb || (row.original.address ? row.original.address.slice(0, 20) + '…' : '—')}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" maxLines={1}>
|
||||
{row.original.city || ''}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'vehicle',
|
||||
accessorKey: 'defaultvehicletype',
|
||||
size: 140,
|
||||
minSize: 110,
|
||||
meta: { label: 'Vehicle' },
|
||||
header: () => <SectionLabel>Vehicle</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<HStack gap={1} vAlign="center">
|
||||
<AccentAvatar color="#0ea5e9" size={22}>
|
||||
<MdTwoWheeler size={12} />
|
||||
</AccentAvatar>
|
||||
<Text type="supporting" weight="bold">
|
||||
{getValue() || '—'}
|
||||
</Text>
|
||||
</HStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'shift',
|
||||
accessorKey: 'shiftid',
|
||||
size: 90,
|
||||
minSize: 70,
|
||||
meta: { label: 'Shift' },
|
||||
header: () => <SectionLabel>Shift</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
#{getValue() ?? '—'}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'time',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
enableSorting: false,
|
||||
meta: { label: 'Time', align: 'center' },
|
||||
header: () => <SectionLabel>Time</SectionLabel>,
|
||||
cell: ({ row }) => (
|
||||
<VStack gap={0.5} padding={0} vAlign="center">
|
||||
<Badge
|
||||
variant="green"
|
||||
label={row.original.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.original.starttime}`).format('hh:mm A') : '—'}
|
||||
/>
|
||||
<Badge
|
||||
variant="red"
|
||||
label={row.original.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.original.endtime}`).format('hh:mm A') : '—'}
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'fare',
|
||||
accessorKey: 'basefare',
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
meta: { label: 'Fare' },
|
||||
header: () => <SectionLabel>Fare</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<Text type="supporting" weight="bold">
|
||||
{getValue() ?? '—'}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'fuel',
|
||||
accessorKey: 'fuelcharge',
|
||||
size: 100,
|
||||
minSize: 80,
|
||||
meta: { label: 'Fuel' },
|
||||
header: () => <SectionLabel>Fuel</SectionLabel>,
|
||||
cell: ({ getValue }) => (
|
||||
<Text type="supporting" weight="bold">
|
||||
{getValue() ?? '—'}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
accessorKey: 'availabilitystatus',
|
||||
size: 130,
|
||||
minSize: 110,
|
||||
meta: { label: 'Status', align: 'center' },
|
||||
header: () => <SectionLabel>Status</SectionLabel>,
|
||||
cell: ({ row }) => {
|
||||
const meta = getRowStatusMeta(row.original);
|
||||
return <Badge variant={meta.badge} icon={<meta.icon size={12} />} label={meta.label} />;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (roleid == 1) {
|
||||
base.push({
|
||||
id: 'action',
|
||||
size: 150,
|
||||
minSize: 120,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
meta: { label: 'Action' },
|
||||
header: () => <SectionLabel>Action</SectionLabel>,
|
||||
cell: ({ row }) => <RowActions row={row.original} expanded={logsRow === row.original.milerprofileid} />
|
||||
});
|
||||
}
|
||||
|
||||
return base;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [roleid, logsRow, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
const table = useTable({
|
||||
features: RIDERS_TABLE_FEATURES,
|
||||
// The full filtered set — the table paginates it, same pattern
|
||||
// orders.js uses for its (also client-filtered) bookings list.
|
||||
data: filteredRows,
|
||||
columns,
|
||||
getRowId: (row, index) => String(row.userid ?? row.milerprofileid ?? `row-${index}`),
|
||||
columnResizeMode: 'onChange',
|
||||
state: { sorting, pagination, columnSizing },
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
onColumnSizingChange: setColumnSizing
|
||||
});
|
||||
|
||||
// Skeleton rows and the empty state span exactly the columns that are
|
||||
// actually rendered — the Action column only exists for roleid 1. Read off
|
||||
// `columns` directly (not `table.getVisibleLeafColumns()`): that method
|
||||
// only exists when columnVisibilityFeature is registered, which this
|
||||
// table doesn't need (unlike orders.js — there is no column-hide menu here).
|
||||
const colCount = columns.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(allRidersLoading || riderSummarysLoading || (tabvalue === 2 && subRidersLogsLoading) || (tabvalue === 3 && subHistoryLoading)) && (
|
||||
@@ -532,19 +786,12 @@ const Riders = () => {
|
||||
</KpiGrid>
|
||||
|
||||
<DataCard
|
||||
// History-only controls (status filter + date) get their own row —
|
||||
// ONLY on that tab, so the other three tabs never show an empty
|
||||
// toolbar strip above the tabs+search row.
|
||||
toolbar={
|
||||
<FilterToolbar max={tabvalue === 3 ? 3 : 1}>
|
||||
<HStack justify="end">
|
||||
<VStack maxWidth={260} width="100%" padding={0} gap={0}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search riders (ctrl+k)"
|
||||
/>
|
||||
</VStack>
|
||||
</HStack>
|
||||
{tabvalue === 3 && (
|
||||
tabvalue === 3 ? (
|
||||
<FilterToolbar max={2}>
|
||||
<HStack gap={2} vAlign="center">
|
||||
<Text weight="bold" color="secondary">
|
||||
Status:
|
||||
@@ -555,8 +802,6 @@ const Riders = () => {
|
||||
))}
|
||||
</SegmentedControl>
|
||||
</HStack>
|
||||
)}
|
||||
{tabvalue === 3 && (
|
||||
<DateInput
|
||||
label="Select Date"
|
||||
isLabelHidden
|
||||
@@ -564,30 +809,45 @@ const Riders = () => {
|
||||
value={historyDate ? dayjs(historyDate).format('YYYY-MM-DD') : undefined}
|
||||
onChange={(v) => v && setHistoryDate(dayjs(v))}
|
||||
/>
|
||||
)}
|
||||
</FilterToolbar>
|
||||
</FilterToolbar>
|
||||
) : undefined
|
||||
}
|
||||
// Tabs and search share ONE row (same pattern orders.js uses) —
|
||||
// previously the search box lived in its own toolbar row above the
|
||||
// tab strip, which read as two stacked rows instead of one control
|
||||
// strip.
|
||||
tabs={
|
||||
<TabList value={String(tabvalue)} onChange={(v) => handleChangetab(Number(v))} size="sm">
|
||||
{TAB_META.map((t) => {
|
||||
const Icon = t.icon;
|
||||
const count =
|
||||
t.key === 2
|
||||
? subRidersLogsData?.length || 0
|
||||
: t.key === 3
|
||||
? subHistoryData?.length || 0
|
||||
: allRidersSummary?.[t.countKey] ?? 0;
|
||||
return (
|
||||
<Tab
|
||||
key={t.key}
|
||||
value={String(t.key)}
|
||||
label={t.label}
|
||||
icon={<Icon size={15} />}
|
||||
endContent={<Badge variant="neutral" label={count} />}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
<HStack justify="between" vAlign="center" gap={2} wrap="wrap">
|
||||
<TabList value={String(tabvalue)} onChange={(v) => handleChangetab(Number(v))} size="sm">
|
||||
{TAB_META.map((t) => {
|
||||
const Icon = t.icon;
|
||||
const count =
|
||||
t.key === 2
|
||||
? subRidersLogsData?.length || 0
|
||||
: t.key === 3
|
||||
? subHistoryData?.length || 0
|
||||
: allRidersSummary?.[t.countKey] ?? 0;
|
||||
return (
|
||||
<Tab
|
||||
key={t.key}
|
||||
value={String(t.key)}
|
||||
label={t.label}
|
||||
icon={<Icon size={15} />}
|
||||
endContent={<Badge variant="neutral" label={count} />}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
|
||||
<VStack maxWidth={260} width="100%" padding={0} gap={0}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search riders (ctrl+k)"
|
||||
/>
|
||||
</VStack>
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
{tabvalue === 3 ? (
|
||||
@@ -769,13 +1029,17 @@ const Riders = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Same paginated + sorted row model the desktop table
|
||||
reads, so a mobile viewer and a desktop viewer never see
|
||||
a different page of riders for the same filters. */}
|
||||
{filteredRows?.length !== 0 &&
|
||||
filteredRows?.map((row, index) => {
|
||||
table.getRowModel().rows.map((tableRow, index) => {
|
||||
const row = tableRow.original;
|
||||
const statusMeta = getRowStatusMeta(row);
|
||||
const expanded = logsRow === row.milerprofileid;
|
||||
return (
|
||||
<MobileCard
|
||||
key={row.userid ?? index}
|
||||
key={tableRow.id ?? index}
|
||||
accent={statusMeta.color}
|
||||
selected={expanded}
|
||||
header={
|
||||
@@ -859,43 +1123,110 @@ const Riders = () => {
|
||||
</MobileCard>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredRows?.length !== 0 && (
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
) : (
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
No more riders
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</MobileCardList>
|
||||
) : (
|
||||
<TableScroll minWidth={1300}>
|
||||
<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 table.
|
||||
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"
|
||||
style={{ tableLayout: 'fixed', width: '100%', minWidth: table.getTotalSize() }}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell style={{ minWidth: 40, width: 40, maxWidth: 40 }}>#</TableHeaderCell>
|
||||
<TableHeaderCell>ID</TableHeaderCell>
|
||||
<TableHeaderCell>Rider</TableHeaderCell>
|
||||
<TableHeaderCell>Address</TableHeaderCell>
|
||||
<TableHeaderCell>Vehicle</TableHeaderCell>
|
||||
<TableHeaderCell>Shift</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center' }}>Time</TableHeaderCell>
|
||||
<TableHeaderCell>Fare</TableHeaderCell>
|
||||
<TableHeaderCell>Fuel</TableHeaderCell>
|
||||
<TableHeaderCell style={{ textAlign: 'center' }}>Status</TableHeaderCell>
|
||||
{roleid == 1 && <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 === '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>
|
||||
{allRidersLoading && <TableLoader rows={6} columns={totalCols} />}
|
||||
{allRidersLoading && <TableLoader rows={6} columns={colCount} />}
|
||||
|
||||
{filteredRows?.length === 0 && !allRidersLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={totalCols}>
|
||||
<TableCell colSpan={colCount}>
|
||||
<EmptyState
|
||||
title="No riders to show"
|
||||
description={
|
||||
@@ -908,110 +1239,32 @@ const Riders = () => {
|
||||
)}
|
||||
|
||||
{filteredRows?.length !== 0 &&
|
||||
filteredRows?.map((row, index) => {
|
||||
const statusMeta = getRowStatusMeta(row);
|
||||
table.getRowModel().rows.map((tableRow) => {
|
||||
const row = tableRow.original;
|
||||
const expanded = logsRow === row.milerprofileid;
|
||||
return (
|
||||
<Fragment key={row.userid ?? index}>
|
||||
<Fragment key={tableRow.id}>
|
||||
<TableRow style={{ backgroundColor: expanded ? tint(BRAND) : undefined }}>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
{/* getAllCells (core), not getVisibleCells —
|
||||
that one is gated behind
|
||||
columnVisibilityFeature, which this table
|
||||
doesn't register. */}
|
||||
{tableRow.getAllCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '3px 8px',
|
||||
borderRadius: 999,
|
||||
background: '#ffffff',
|
||||
border: `1px solid ${edge(BRAND)}`,
|
||||
color: BRAND,
|
||||
fontWeight: 800,
|
||||
fontSize: 11,
|
||||
minWidth: 56
|
||||
textAlign: cell.column.columnDef.meta?.align,
|
||||
maxWidth: cell.column.columnDef.maxSize || cell.column.getSize()
|
||||
}}
|
||||
>
|
||||
#{row?.userid}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<VStack gap={0} padding={0} minWidth={0}>
|
||||
<Text weight="semibold" style={{ whiteSpace: 'nowrap' }}>
|
||||
{row.displayname || row.authname || '—'}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary">
|
||||
{row.phone || '—'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: 220 }}>
|
||||
<Tooltip content={row.address || ''}>
|
||||
<VStack gap={0} padding={0}>
|
||||
<Text type="supporting" weight="bold" color="secondary" maxLines={1}>
|
||||
{row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')}
|
||||
</Text>
|
||||
<Text type="supporting" color="secondary" maxLines={1}>
|
||||
{row.city || ''}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<HStack gap={1} vAlign="center">
|
||||
<AccentAvatar color="#0ea5e9" size={22}>
|
||||
<MdTwoWheeler size={12} />
|
||||
</AccentAvatar>
|
||||
<Text type="supporting" weight="bold">
|
||||
{row.defaultvehicletype || '—'}
|
||||
</Text>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
#{row.shiftid ?? '—'}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<VStack gap={0.5} padding={0} vAlign="center">
|
||||
<Badge
|
||||
variant="green"
|
||||
label={
|
||||
row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'
|
||||
}
|
||||
/>
|
||||
<Badge
|
||||
variant="red"
|
||||
label={row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'}
|
||||
/>
|
||||
</VStack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold">
|
||||
{row.basefare ?? '—'}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text type="supporting" weight="bold">
|
||||
{row.fuelcharge ?? '—'}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'center' }}>
|
||||
<Badge variant={statusMeta.badge} icon={<statusMeta.icon size={12} />} label={statusMeta.label} />
|
||||
</TableCell>
|
||||
{roleid == 1 && (
|
||||
<TableCell>
|
||||
<RowActions row={row} expanded={expanded} />
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
)}
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{expanded && tabvalue !== 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={totalCols} style={{ padding: 12 }}>
|
||||
<TableCell colSpan={colCount} style={{ padding: 12 }}>
|
||||
<TelemetryPanel row={row} riderLogsdata={riderLogsdata} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -1019,26 +1272,39 @@ const Riders = () => {
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredRows?.length !== 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={totalCols}>
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<LoaderWithImage />
|
||||
) : (
|
||||
<Text type="supporting" weight="bold" color="secondary">
|
||||
No more riders
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
)}
|
||||
|
||||
{/* Pagination replaces the old scroll-to-reveal sentinel — the
|
||||
API returns the full rider list in one shot, so this was
|
||||
always paginating an in-memory array, just without any UI
|
||||
for it. Shared between mobile and desktop since both read
|
||||
the same `table` row model above. */}
|
||||
{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={(nextPage) => table.setPageIndex(nextPage - 1)}
|
||||
totalItems={filteredRows.length}
|
||||
pageSize={pagination.pageSize}
|
||||
pageSizeOptions={RIDERS_PAGE_SIZE_OPTIONS}
|
||||
onPageSizeChange={(size) => table.setPageSize(size)}
|
||||
size="sm"
|
||||
label="Riders pagination"
|
||||
/>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DataCard>
|
||||
|
||||
Reference in New Issue
Block a user