feat(warehouses): add import-ops stat tiles and arrival-queue filters

Adds Arrived/Unloaded/Dispatch Ready/Total Bookings stat tiles above the
import queue tabs, wires search+date filters and pagination into the
arrival queue table (reusing useListControls/ListControls/
RuleEngineListFooter already used by Inventory Inquiry), and adds a
copy-to-clipboard action on the truncated schedule ID.
This commit is contained in:
Hagernesh
2026-08-20 10:15:27 +00:00
parent 2f35b0a717
commit d4c51ec63a

View File

@@ -4,7 +4,9 @@ import {
Alert, Alert,
Badge, Badge,
Button, Button,
Card,
Checkbox, Checkbox,
CopyButton,
Group, Group,
Loader, Loader,
Menu, Menu,
@@ -12,20 +14,25 @@ import {
NumberInput, NumberInput,
ScrollArea, ScrollArea,
Select, Select,
SimpleGrid,
Stack, Stack,
Table, Table,
Tabs, Tabs,
Text, Text,
Textarea, Textarea,
TextInput, TextInput,
ThemeIcon,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { import {
ArrowRightLeft, ArrowRightLeft,
Calendar,
Check, Check,
CheckCheck,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
ClipboardCheck, ClipboardCheck,
Copy,
Eye, Eye,
FileText, FileText,
History, History,
@@ -83,6 +90,9 @@ import { StoreInventoryModal } from './StoreInventoryModal';
import { WarehouseInquiryTable } from './WarehouseInquiryTable'; import { WarehouseInquiryTable } from './WarehouseInquiryTable';
import { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions, warehousesAtStation, yardsForBooking } from './options'; import { extractDownloadErrorMessage, extractErrorMessage, formatDate, formatNumber, inventoryStatusOptions, warehousesAtStation, yardsForBooking } from './options';
import { openPdfBlob } from './pdf'; import { openPdfBlob } from './pdf';
import ListControls from '@/components/common/ListControls';
import RuleEngineListFooter from '@/components/ruleEngine/RuleEngineListFooter';
import { useListControls } from '@/hooks/useListControls';
import '@/components/overview/overview.css'; import '@/components/overview/overview.css';
type ImportUnloadAssignment = { bookingId: string; warehouseId: string; yardId: string; zoneId: string }; type ImportUnloadAssignment = { bookingId: string; warehouseId: string; yardId: string; zoneId: string };
@@ -2218,6 +2228,11 @@ export function ImportArriveQueueTab({
>({}); >({});
const [readyBySchedule, setReadyBySchedule] = useState<Record<string, boolean>>({}); const [readyBySchedule, setReadyBySchedule] = useState<Record<string, boolean>>({});
const controls = useListControls(trains, {
searchKeys: ['trainNumber', 'route', 'origin', 'destination', 'status'],
dateKey: 'arrivalTime',
});
const autoUnload = async (train: ImportTrain) => { const autoUnload = async (train: ImportTrain) => {
const assignments = Object.entries(assignmentsBySchedule[train.scheduleId] ?? {}) const assignments = Object.entries(assignmentsBySchedule[train.scheduleId] ?? {})
.filter((entry): entry is [string, Required<ImportUnloadAssignmentDraft>] => .filter((entry): entry is [string, Required<ImportUnloadAssignmentDraft>] =>
@@ -2272,10 +2287,6 @@ export function ImportArriveQueueTab({
return ( return (
<Stack gap="sm" mt="sm"> <Stack gap="sm" mt="sm">
<Text size="sm" c="dimmed">
<b>{trains.length}</b> arrived import train{trains.length !== 1 ? 's' : ''}
</Text>
{isLoading ? ( {isLoading ? (
<Group justify="center" py="lg"> <Group justify="center" py="lg">
<Loader size="sm" /> <Loader size="sm" />
@@ -2285,6 +2296,25 @@ export function ImportArriveQueueTab({
No arrived import trains. Trains appear here once their schedule status is ARRIVED. No arrived import trains. Trains appear here once their schedule status is ARRIVED.
</Text> </Text>
) : ( ) : (
<Stack gap="sm">
<Group justify="space-between" align="flex-end" wrap="wrap">
<Text size="sm" c="dimmed">
<b>{controls.totalCount}</b> arrived import train{controls.totalCount !== 1 ? 's' : ''}
</Text>
<ListControls
search={controls.search}
onSearchChange={controls.setSearch}
searchPlaceholder="Train #, route, origin, destination…"
dateFrom={controls.dateFrom}
onDateFromChange={controls.setDateFrom}
dateTo={controls.dateTo}
onDateToChange={controls.setDateTo}
dateLabel="Arrival"
hasFilters={controls.hasFilters}
onReset={controls.reset}
/>
</Group>
<Table.ScrollContainer minWidth={1500}> <Table.ScrollContainer minWidth={1500}>
<Table highlightOnHover verticalSpacing="xs" striped> <Table highlightOnHover verticalSpacing="xs" striped>
<Table.Thead> <Table.Thead>
@@ -2303,7 +2333,16 @@ export function ImportArriveQueueTab({
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{trains.map((t: ImportTrain) => { {controls.pagedRows.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={11}>
<Text c="dimmed" ta="center" py="lg" size="sm">
No trains match the current filters.
</Text>
</Table.Td>
</Table.Tr>
) : (
controls.pagedRows.map((t: ImportTrain) => {
const isOpen = openId === t.scheduleId; const isOpen = openId === t.scheduleId;
const fullyUnloaded = isFullyUnloaded(t); const fullyUnloaded = isFullyUnloaded(t);
const unloadedBookings = t.unloadedBookings ?? t.totalBookings - getPendingUnloadBookings(t); const unloadedBookings = t.unloadedBookings ?? t.totalBookings - getPendingUnloadBookings(t);
@@ -2311,7 +2350,18 @@ export function ImportArriveQueueTab({
<Fragment key={t.scheduleId}> <Fragment key={t.scheduleId}>
<Table.Tr> <Table.Tr>
<Table.Td> <Table.Td>
<Text size="xs" c="dimmed">{t.scheduleId.slice(0, 8)}</Text> <Group gap={4} wrap="nowrap">
<Text size="xs" c="dimmed">{t.scheduleId.slice(0, 8)}</Text>
<CopyButton value={t.scheduleId}>
{({ copied, copy }) => (
<Tooltip label={copied ? 'Copied' : 'Copy schedule ID'} withArrow>
<ActionIcon size="xs" variant="subtle" color={copied ? 'teal' : 'gray'} onClick={copy}>
{copied ? <CheckCheck size={12} /> : <Copy size={12} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="sm" fw={600}>{t.trainNumber ?? '—'}</Text> <Text size="sm" fw={600}>{t.trainNumber ?? '—'}</Text>
@@ -2390,10 +2440,19 @@ export function ImportArriveQueueTab({
)} )}
</Fragment> </Fragment>
); );
})} }))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Table.ScrollContainer> </Table.ScrollContainer>
<RuleEngineListFooter
pagination={controls.pagination}
pageCount={controls.pageCount}
totalCount={controls.totalCount}
itemLabel="trains"
onPaginationChange={controls.setPagination}
/>
</Stack>
)} )}
<ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} /> <ConfirmActionModal action={confirmAction} onClose={() => setConfirmAction(null)} />
</Stack> </Stack>
@@ -2872,6 +2931,41 @@ interface WarehouseFlowWorkbenchProps {
focusedBookingLabel?: string; focusedBookingLabel?: string;
} }
function ImportStatCard({
icon,
label,
value,
sub,
color,
}: {
icon: React.ReactNode;
label: string;
value: React.ReactNode;
sub: string;
color: string;
}) {
return (
<Card withBorder radius="md" padding="md">
<Group gap="sm" wrap="nowrap">
<ThemeIcon color={color} variant="light" size={40} radius="md">
{icon}
</ThemeIcon>
<Stack gap={0} style={{ minWidth: 0 }}>
<Text fw={800} fz={22} lh={1.1}>
{value}
</Text>
<Text size="sm" fw={600}>
{label}
</Text>
<Text size="xs" c="dimmed">
{sub}
</Text>
</Stack>
</Group>
</Card>
);
}
function WarehouseQueueTabs<TValue extends string>({ function WarehouseQueueTabs<TValue extends string>({
value, value,
onChange, onChange,
@@ -3039,6 +3133,7 @@ function ImportWarehouseTabs({ enabled, onChanged }: { enabled: boolean; onChang
const { data: arriveRows = [] } = useQuery(api.warehouses.importArriveQueue.queryOptions({ enabled })); const { data: arriveRows = [] } = useQuery(api.warehouses.importArriveQueue.queryOptions({ enabled }));
const { data: unloadedRows = [] } = useQuery(api.warehouses.importUnloadedQueue.queryOptions({ enabled })); const { data: unloadedRows = [] } = useQuery(api.warehouses.importUnloadedQueue.queryOptions({ enabled }));
const { data: dispatchRows = [] } = useQuery(api.warehouses.importPickupReadyQueue.queryOptions({ enabled })); const { data: dispatchRows = [] } = useQuery(api.warehouses.importPickupReadyQueue.queryOptions({ enabled }));
const totalBookings = arriveRows.reduce((sum, t) => sum + t.totalBookings, 0);
const tabs: WarehouseQueueTab<ImportWarehouseTab>[] = [ const tabs: WarehouseQueueTab<ImportWarehouseTab>[] = [
{ {
value: 'arrive-queue', value: 'arrive-queue',
@@ -3067,6 +3162,13 @@ 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">
<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" />
</SimpleGrid>
<WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} /> <WarehouseQueueTabs value={activeTab} onChange={setActiveTab} tabs={tabs} />
{activeTab === 'arrive-queue' && ( {activeTab === 'arrive-queue' && (