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],
);
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 someSelected = selected.size > 0 && !allSelected;
const pendingReceiveRows = useMemo(
@@ -1085,7 +1088,7 @@ function EligibleTab({
<Group justify="space-between">
<Text size="sm" c="dimmed">
Selected: <b>{selected.size}</b> / {statusFilteredRows.length} eligible
Selected: <b>{selected.size}</b> / {controls.filteredRows.length} eligible
</Text>
<Group gap="xs">
<Button
@@ -1128,6 +1131,19 @@ function EligibleTab({
: `No eligible PAID ${direction.toLowerCase()} bookings to receive.`}
</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 highlightOnHover verticalSpacing="xs" striped>
<Table.Thead>
@@ -1156,7 +1172,7 @@ function EligibleTab({
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{statusFilteredRows.map((r) => {
{controls.pagedRows.map((r) => {
const canReceive = canReceiveBooking(r);
return (
<Table.Tr key={r.id}>
@@ -1252,6 +1268,14 @@ function EligibleTab({
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="bookings"
onPaginationChange={controls.setPagination}
/>
</Stack>
)}
<Modal
@@ -1350,7 +1374,10 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
const [expandedRow, setExpandedRow] = 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 someSelected = selected.size > 0 && !allSelected;
const toggleAll = () =>
@@ -1414,6 +1441,19 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
No received export items awaiting inspection.
</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 highlightOnHover verticalSpacing="xs" striped>
<Table.Thead>
@@ -1440,7 +1480,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((r: ReadyToLoadRow) => {
{controls.pagedRows.map((r: ReadyToLoadRow) => {
const selectable = r.inspectionStatus !== 'PASSED';
return (
<Fragment key={r.id}>
@@ -1503,6 +1543,14 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="items"
onPaginationChange={controls.setPagination}
/>
</Stack>
)}
<InspectionReportModal
@@ -1527,9 +1575,12 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null);
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 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) =>
setSelected((prev) => {
const next = new Set(prev);
@@ -1599,9 +1650,9 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
<Group justify="space-between">
<Text size="sm" c="dimmed">
{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>
<Button
@@ -1670,6 +1721,19 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
No EXPORT items with inspection PASSED waiting to be loaded.
</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 highlightOnHover verticalSpacing="xs" striped>
<Table.Thead>
@@ -1695,7 +1759,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((r: ReadyToLoadRow) => (
{controls.pagedRows.map((r: ReadyToLoadRow) => (
<Fragment key={r.id}>
<Table.Tr>
<Table.Td>
@@ -1749,6 +1813,14 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="items"
onPaginationChange={controls.setPagination}
/>
</Stack>
)}
</Stack>
);
@@ -1778,9 +1850,12 @@ function LoadedExportTab({
);
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 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) =>
setSelected((prev) => {
const next = new Set(prev);
@@ -1812,11 +1887,11 @@ function LoadedExportTab({
<Text size="sm" c="dimmed">
{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>
@@ -1868,6 +1943,19 @@ function LoadedExportTab({
No LOADED export items {dispatchable ? 'waiting to dispatch' : 'yet'}.
</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 highlightOnHover verticalSpacing="xs" striped>
<Table.Thead>
@@ -1894,7 +1982,7 @@ function LoadedExportTab({
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((r: ReadyToLoadRow) => (
{controls.pagedRows.map((r: ReadyToLoadRow) => (
<Fragment key={r.id}>
<Table.Tr>
{dispatchable && (
@@ -1945,6 +2033,14 @@ function LoadedExportTab({
</Table.Tbody>
</Table>
</Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="items"
onPaginationChange={controls.setPagination}
/>
</Stack>
)}
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
</Stack>
@@ -2931,7 +3027,7 @@ interface WarehouseFlowWorkbenchProps {
focusedBookingLabel?: string;
}
function ImportStatCard({
function WarehouseStatCard({
icon,
label,
value,
@@ -3033,6 +3129,7 @@ function LocateBookingTab({ enabled }: { enabled: boolean }) {
applied.status,
);
const { data: results = [], isFetching } = useInventoryInquiry(applied, enabled && hasSearch);
const controls = useListControls(results);
const normalizeDraft = (): InventoryInquiryFilter => ({
bookingReference: draft.bookingReference?.trim() || undefined,
@@ -3116,7 +3213,16 @@ function LocateBookingTab({ enabled }: { enabled: boolean }) {
No inventory found for the current filters.
</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
@@ -3163,10 +3269,10 @@ function ImportWarehouseTabs({ enabled, onChanged }: { enabled: boolean; onChang
return (
<Stack gap="md">
<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" />
<ImportStatCard 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" />
<ImportStatCard icon={<Calendar size={18} />} label="Total Bookings" value={totalBookings} sub="Across arrived trains" color="orange" />
<WarehouseStatCard icon={<PackageOpen size={18} />} label="Arrived" value={arriveRows.length} sub="Import trains" color="edr-green" />
<WarehouseStatCard icon={<ClipboardCheck size={18} />} label="Unloaded" value={unloadedRows.length} sub="Import trains" color="blue" />
<WarehouseStatCard icon={<Send size={18} />} label="Dispatch Ready" value={dispatchRows.length} sub="Import trains" color="violet" />
<WarehouseStatCard icon={<Calendar size={18} />} label="Total Bookings" value={totalBookings} sub="Across arrived trains" color="orange" />
</SimpleGrid>
<WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} />
@@ -3254,6 +3360,14 @@ function ExportWarehouseTabs({
return (
<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} />
{activeTab === 'receive-queue' && (