mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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:
@@ -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' && (
|
||||
|
||||
Reference in New Issue
Block a user