feat(warehouses): filters/pagination on all import+export queue tabs

Extends the stat-tile/filter/pagination pattern from the import arrival
queue to every remaining import and export tab (unloaded, eligible,
received, ready-to-load, loaded/dispatch, locate-booking), reusing
useListControls/ListControls/RuleEngineListFooter throughout. Adds a
5-tile stat row to the export tab bar and renames ImportStatCard to
WarehouseStatCard now that it's shared by both directions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-08-20 10:25:08 +00:00
parent d4c51ec63a
commit f9987e515c

View File

@@ -925,7 +925,10 @@ function EligibleTab({
), ),
[rows, statusOptions], [rows, statusOptions],
); );
const selectableRows = statusFilteredRows.filter(canReceiveBooking); const controls = useListControls(statusFilteredRows, {
searchKeys: ['reference', 'customer', 'origin', 'destination', 'containerNumber', 'cargo', 'cargoDescription'],
});
const selectableRows = controls.filteredRows.filter(canReceiveBooking);
const allSelected = selectableRows.length > 0 && selected.size === selectableRows.length; const allSelected = selectableRows.length > 0 && selected.size === selectableRows.length;
const someSelected = selected.size > 0 && !allSelected; const someSelected = selected.size > 0 && !allSelected;
const pendingReceiveRows = useMemo( const pendingReceiveRows = useMemo(
@@ -1085,7 +1088,7 @@ function EligibleTab({
<Group justify="space-between"> <Group justify="space-between">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Selected: <b>{selected.size}</b> / {statusFilteredRows.length} eligible Selected: <b>{selected.size}</b> / {controls.filteredRows.length} eligible
</Text> </Text>
<Group gap="xs"> <Group gap="xs">
<Button <Button
@@ -1128,6 +1131,19 @@ function EligibleTab({
: `No eligible PAID ${direction.toLowerCase()} bookings to receive.`} : `No eligible PAID ${direction.toLowerCase()} bookings to receive.`}
</Text> </Text>
) : ( ) : (
<Stack gap="sm">
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
searchPlaceholder="Booking, customer, route, container, cargo…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
hasFilters={controls.hasFilters}
onReset={controls.reset}
showDateRange={false}
/>
<Table.ScrollContainer minWidth={1350}> <Table.ScrollContainer minWidth={1350}>
<Table highlightOnHover verticalSpacing="xs" striped> <Table highlightOnHover verticalSpacing="xs" striped>
<Table.Thead> <Table.Thead>
@@ -1156,7 +1172,7 @@ function EligibleTab({
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{statusFilteredRows.map((r) => { {controls.pagedRows.map((r) => {
const canReceive = canReceiveBooking(r); const canReceive = canReceiveBooking(r);
return ( return (
<Table.Tr key={r.id}> <Table.Tr key={r.id}>
@@ -1252,6 +1268,14 @@ function EligibleTab({
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="bookings"
onPaginationChange={controls.setPagination}
/>
</Stack>
)} )}
<Modal <Modal
@@ -1350,7 +1374,10 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
const [expandedRow, setExpandedRow] = useState<string | null>(null); const [expandedRow, setExpandedRow] = useState<string | null>(null);
const [inspectId, setInspectId] = useState<string | null>(null); const [inspectId, setInspectId] = useState<string | null>(null);
const pendingRows = rows.filter((r) => r.inspectionStatus !== 'PASSED'); const controls = useListControls(rows, {
searchKeys: ['bookingReference', 'customerName', 'containerNumber', 'cargoType', 'grnNumber', 'origin', 'destination'],
});
const pendingRows = controls.filteredRows.filter((r) => r.inspectionStatus !== 'PASSED');
const allSelected = pendingRows.length > 0 && selected.size === pendingRows.length; const allSelected = pendingRows.length > 0 && selected.size === pendingRows.length;
const someSelected = selected.size > 0 && !allSelected; const someSelected = selected.size > 0 && !allSelected;
const toggleAll = () => const toggleAll = () =>
@@ -1414,6 +1441,19 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
No received export items awaiting inspection. No received export items awaiting inspection.
</Text> </Text>
) : ( ) : (
<Stack gap="sm">
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
searchPlaceholder="Booking, GRN, customer, container, cargo, route…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
hasFilters={controls.hasFilters}
onReset={controls.reset}
showDateRange={false}
/>
<Table.ScrollContainer minWidth={1350}> <Table.ScrollContainer minWidth={1350}>
<Table highlightOnHover verticalSpacing="xs" striped> <Table highlightOnHover verticalSpacing="xs" striped>
<Table.Thead> <Table.Thead>
@@ -1440,7 +1480,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{rows.map((r: ReadyToLoadRow) => { {controls.pagedRows.map((r: ReadyToLoadRow) => {
const selectable = r.inspectionStatus !== 'PASSED'; const selectable = r.inspectionStatus !== 'PASSED';
return ( return (
<Fragment key={r.id}> <Fragment key={r.id}>
@@ -1503,6 +1543,14 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="items"
onPaginationChange={controls.setPagination}
/>
</Stack>
)} )}
<InspectionReportModal <InspectionReportModal
@@ -1527,9 +1575,12 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null); const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null);
const [selected, setSelected] = useState<Set<string>>(new Set()); const [selected, setSelected] = useState<Set<string>>(new Set());
const allSelected = rows.length > 0 && selected.size === rows.length; const controls = useListControls(rows, {
searchKeys: ['bookingReference', 'customerName', 'containerNumber', 'cargoType', 'grnNumber', 'origin', 'destination'],
});
const allSelected = controls.filteredRows.length > 0 && selected.size === controls.filteredRows.length;
const someSelected = selected.size > 0 && !allSelected; const someSelected = selected.size > 0 && !allSelected;
const toggleAll = () => setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id))); const toggleAll = () => setSelected(allSelected ? new Set() : new Set(controls.filteredRows.map((r) => r.id)));
const toggleOne = (id: string) => const toggleOne = (id: string) =>
setSelected((prev) => { setSelected((prev) => {
const next = new Set(prev); const next = new Set(prev);
@@ -1599,9 +1650,9 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
<Group justify="space-between"> <Group justify="space-between">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{selected.size > 0 ? ( {selected.size > 0 ? (
<><b>{selected.size}</b> of {rows.length} selected</> <><b>{selected.size}</b> of {controls.filteredRows.length} selected</>
) : ( ) : (
<><b>{rows.length}</b> item{rows.length !== 1 ? 's' : ''} ready to load</> <><b>{controls.filteredRows.length}</b> item{controls.filteredRows.length !== 1 ? 's' : ''} ready to load</>
)} )}
</Text> </Text>
<Button <Button
@@ -1670,6 +1721,19 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
No EXPORT items with inspection PASSED waiting to be loaded. No EXPORT items with inspection PASSED waiting to be loaded.
</Text> </Text>
) : ( ) : (
<Stack gap="sm">
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
searchPlaceholder="Booking, GRN, customer, container, cargo, route…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
hasFilters={controls.hasFilters}
onReset={controls.reset}
showDateRange={false}
/>
<Table.ScrollContainer minWidth={1200}> <Table.ScrollContainer minWidth={1200}>
<Table highlightOnHover verticalSpacing="xs" striped> <Table highlightOnHover verticalSpacing="xs" striped>
<Table.Thead> <Table.Thead>
@@ -1695,7 +1759,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{rows.map((r: ReadyToLoadRow) => ( {controls.pagedRows.map((r: ReadyToLoadRow) => (
<Fragment key={r.id}> <Fragment key={r.id}>
<Table.Tr> <Table.Tr>
<Table.Td> <Table.Td>
@@ -1749,6 +1813,14 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="items"
onPaginationChange={controls.setPagination}
/>
</Stack>
)} )}
</Stack> </Stack>
); );
@@ -1778,9 +1850,12 @@ function LoadedExportTab({
); );
const [selected, setSelected] = useState<Set<string>>(new Set()); const [selected, setSelected] = useState<Set<string>>(new Set());
const allSelected = rows.length > 0 && selected.size === rows.length; const controls = useListControls(rows, {
searchKeys: ['bookingReference', 'customerName', 'containerNumber', 'cargoType', 'grnNumber', 'origin', 'destination'],
});
const allSelected = controls.filteredRows.length > 0 && selected.size === controls.filteredRows.length;
const someSelected = selected.size > 0 && !allSelected; const someSelected = selected.size > 0 && !allSelected;
const toggleAll = () => setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id))); const toggleAll = () => setSelected(allSelected ? new Set() : new Set(controls.filteredRows.map((r) => r.id)));
const toggleOne = (id: string) => const toggleOne = (id: string) =>
setSelected((prev) => { setSelected((prev) => {
const next = new Set(prev); const next = new Set(prev);
@@ -1812,11 +1887,11 @@ function LoadedExportTab({
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{dispatchable ? ( {dispatchable ? (
<> <>
Selected: <b>{selected.size}</b> / {rows.length} loaded Selected: <b>{selected.size}</b> / {controls.filteredRows.length} loaded
</> </>
) : ( ) : (
<> <>
<b>{rows.length}</b> item{rows.length !== 1 ? 's' : ''} loaded <b>{controls.filteredRows.length}</b> item{controls.filteredRows.length !== 1 ? 's' : ''} loaded
</> </>
)} )}
</Text> </Text>
@@ -1868,6 +1943,19 @@ function LoadedExportTab({
No LOADED export items {dispatchable ? 'waiting to dispatch' : 'yet'}. No LOADED export items {dispatchable ? 'waiting to dispatch' : 'yet'}.
</Text> </Text>
) : ( ) : (
<Stack gap="sm">
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
searchPlaceholder="Booking, GRN, customer, container, cargo, route…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
hasFilters={controls.hasFilters}
onReset={controls.reset}
showDateRange={false}
/>
<Table.ScrollContainer minWidth={1200}> <Table.ScrollContainer minWidth={1200}>
<Table highlightOnHover verticalSpacing="xs" striped> <Table highlightOnHover verticalSpacing="xs" striped>
<Table.Thead> <Table.Thead>
@@ -1894,7 +1982,7 @@ function LoadedExportTab({
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{rows.map((r: ReadyToLoadRow) => ( {controls.pagedRows.map((r: ReadyToLoadRow) => (
<Fragment key={r.id}> <Fragment key={r.id}>
<Table.Tr> <Table.Tr>
{dispatchable && ( {dispatchable && (
@@ -1945,6 +2033,14 @@ function LoadedExportTab({
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="items"
onPaginationChange={controls.setPagination}
/>
</Stack>
)} )}
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} /> <ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
</Stack> </Stack>
@@ -2931,7 +3027,7 @@ interface WarehouseFlowWorkbenchProps {
focusedBookingLabel?: string; focusedBookingLabel?: string;
} }
function ImportStatCard({ function WarehouseStatCard({
icon, icon,
label, label,
value, value,
@@ -3033,6 +3129,7 @@ function LocateBookingTab({ enabled }: { enabled: boolean }) {
applied.status, applied.status,
); );
const { data: results = [], isFetching } = useInventoryInquiry(applied, enabled && hasSearch); const { data: results = [], isFetching } = useInventoryInquiry(applied, enabled && hasSearch);
const controls = useListControls(results);
const normalizeDraft = (): InventoryInquiryFilter => ({ const normalizeDraft = (): InventoryInquiryFilter => ({
bookingReference: draft.bookingReference?.trim() || undefined, bookingReference: draft.bookingReference?.trim() || undefined,
@@ -3116,7 +3213,16 @@ function LocateBookingTab({ enabled }: { enabled: boolean }) {
No inventory found for the current filters. No inventory found for the current filters.
</Text> </Text>
) : ( ) : (
<WarehouseInquiryTable results={results} onView={setViewResult} /> <Stack gap="sm">
<WarehouseInquiryTable results={controls.pagedRows} onView={setViewResult} />
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="results"
onPaginationChange={controls.setPagination}
/>
</Stack>
)} )}
<InventoryInquiryDetailModal <InventoryInquiryDetailModal
@@ -3163,10 +3269,10 @@ function ImportWarehouseTabs({ enabled, onChanged }: { enabled: boolean; onChang
return ( return (
<Stack gap="md"> <Stack gap="md">
<SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="sm"> <SimpleGrid cols={{ base: 1, xs: 2, md: 4 }} spacing="sm">
<ImportStatCard icon={<PackageOpen size={18} />} label="Arrived" value={arriveRows.length} sub="Import trains" color="edr-green" /> <WarehouseStatCard icon={<PackageOpen size={18} />} label="Arrived" value={arriveRows.length} sub="Import trains" color="edr-green" />
<ImportStatCard icon={<ClipboardCheck size={18} />} label="Unloaded" value={unloadedRows.length} sub="Import trains" color="blue" /> <WarehouseStatCard icon={<ClipboardCheck size={18} />} label="Unloaded" value={unloadedRows.length} sub="Import trains" color="blue" />
<ImportStatCard icon={<Send size={18} />} label="Dispatch Ready" value={dispatchRows.length} sub="Import trains" color="violet" /> <WarehouseStatCard icon={<Send size={18} />} label="Dispatch Ready" value={dispatchRows.length} sub="Import trains" color="violet" />
<ImportStatCard icon={<Calendar size={18} />} label="Total Bookings" value={totalBookings} sub="Across arrived trains" color="orange" /> <WarehouseStatCard icon={<Calendar size={18} />} label="Total Bookings" value={totalBookings} sub="Across arrived trains" color="orange" />
</SimpleGrid> </SimpleGrid>
<WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} /> <WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} />
@@ -3254,6 +3360,14 @@ function ExportWarehouseTabs({
return ( return (
<Stack gap="md"> <Stack gap="md">
<SimpleGrid cols={{ base: 1, xs: 2, md: 5 }} spacing="sm">
<WarehouseStatCard icon={<Truck size={18} />} label="Eligible" value={exportEligibleCount} sub="Export bookings" color="edr-green" />
<WarehouseStatCard icon={<ClipboardCheck size={18} />} label="Received" value={receivedRows.length} sub="Export bookings" color="blue" />
<WarehouseStatCard icon={<Train size={18} />} label="Ready To Load" value={readyRows.length} sub="Export bookings" color="teal" />
<WarehouseStatCard icon={<PackageCheck size={18} />} label="Loaded" value={loadedRows.length} sub="Export bookings" color="indigo" />
<WarehouseStatCard icon={<Send size={18} />} label="Dispatch Ready" value={loadedRows.length} sub="Export bookings" color="violet" />
</SimpleGrid>
<WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} /> <WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} />
{activeTab === 'receive-queue' && ( {activeTab === 'receive-queue' && (