mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
feat(warehouse): Batch 9 — Import Unloaded Queue + bulk destination inspection
- bulkMarkInspected: UNLOADED added to eligible statuses; IMPORT passed items now advance to READY_FOR_PICKUP (pickup ready) instead of READY_FOR_LOADING (export unchanged) - GET /warehouse-inventory/import/unloaded-queue: UNLOADED import items with the inspection-screen columns (booking, customer, arrival, container, cargo type, weight, train schedule, inspection status, pickup option, last-mile, current status); route-derived import filter - Dedicated ImportUnloadedQueueTab replaces the Batch 8 workbench reuse: full spec columns, Select All / Unselect All / selected count / Mark Selected as Inspected, plus per-row Inspect / Report (InspectionReportModal) for damage / image / weight-loss detail - Verified: import item UNLOADED → bulk inspect → READY_FOR_PICKUP (PASSED), drops out of queue Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,21 +16,22 @@ import {
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { ChevronDown, ChevronRight, Info, PackageSearch, Train, Truck } from 'lucide-react';
|
||||
import { ChevronDown, ChevronRight, ClipboardCheck, Info, PackageSearch, Train, Truck } from 'lucide-react';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
useAutoUnloadArrivedBookings,
|
||||
useBulkDispatchExport,
|
||||
useBulkMarkInspected,
|
||||
useBulkReceive,
|
||||
useEligibleBookings,
|
||||
useImportArriveQueue,
|
||||
useImportTrainItems,
|
||||
useImportUnloadedQueue,
|
||||
useLoadPassedExport,
|
||||
useLoadedExport,
|
||||
useReadyToLoadExport,
|
||||
useReceiveInventory,
|
||||
useWarehouseInventory,
|
||||
useWarehouseYards,
|
||||
useWarehouseZones,
|
||||
useWarehouses,
|
||||
@@ -38,15 +39,17 @@ import {
|
||||
import type {
|
||||
AutoUnloadArrivedResult,
|
||||
BulkDispatchResult,
|
||||
BulkInspectResult,
|
||||
BulkReceiveResult,
|
||||
ImportTrain,
|
||||
ImportTrainItem,
|
||||
ImportUnloadedItem,
|
||||
LoadPassedExportResult,
|
||||
ReadyToLoadRow,
|
||||
ReceiveInventoryPayload,
|
||||
} from '@/types/warehouse';
|
||||
import { BookingSelect } from './BookingSelect';
|
||||
import { InventoryWorkbench } from './InventoryWorkbench';
|
||||
import { InspectionReportModal } from './InspectionReportModal';
|
||||
import { extractErrorMessage, formatDate, formatNumber } from './options';
|
||||
|
||||
interface ReceiveInventoryModalProps {
|
||||
@@ -855,28 +858,166 @@ function ImportArriveQueueTab({
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Unloaded Queue: items unloaded off arrived trains (status UNLOADED), with the full set of
|
||||
* lifecycle actions (Inspect / Store / Move / History / …) plus a Last Mile action shown only when
|
||||
* the booking requested door delivery. No automatic storage happens here — the operator drives it.
|
||||
* Import Unloaded Queue (Batch 9): all unloaded import items with the destination-inspection columns.
|
||||
* Multi-select + Mark Selected as Inspected (import passed → READY_FOR_PICKUP), and the per-item
|
||||
* Inspect / Report action stays for damage / images / weight-loss detail.
|
||||
*/
|
||||
function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
const { toast } = useToast();
|
||||
const { data: items = [], isLoading } = useWarehouseInventory(enabled ? { status: 'UNLOADED' } : undefined);
|
||||
const { data: rows = [], isLoading } = useImportUnloadedQueue(enabled);
|
||||
const inspectMutation = useBulkMarkInspected();
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||
|
||||
const allSelected = rows.length > 0 && selected.size === rows.length;
|
||||
const someSelected = selected.size > 0 && !allSelected;
|
||||
const selectAll = () => setSelected(new Set(rows.map((r) => r.id)));
|
||||
const unselectAll = () => setSelected(new Set());
|
||||
const toggleOne = (id: string) =>
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
return next;
|
||||
});
|
||||
|
||||
const markInspected = async () => {
|
||||
if (selected.size === 0) {
|
||||
toast({ variant: 'destructive', title: 'Select at least one item' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = (await inspectMutation.mutateAsync({ inventoryIds: [...selected] })) as {
|
||||
data: BulkInspectResult;
|
||||
};
|
||||
const r = res.data;
|
||||
toast({
|
||||
title: `${r.inspectedCount} marked inspected`,
|
||||
description: r.skippedCount ? `${r.skippedCount} skipped` : undefined,
|
||||
});
|
||||
setSelected(new Set());
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Mark inspected failed', description: extractErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="sm" mt="sm">
|
||||
<Text size="sm" c="dimmed">
|
||||
<b>{items.length}</b> unloaded item{items.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
<InventoryWorkbench
|
||||
items={items}
|
||||
isLoading={isLoading}
|
||||
onLastMile={(it) =>
|
||||
toast({
|
||||
title: 'Last mile delivery',
|
||||
description: `Door delivery for ${it.booking?.reference ?? it.id} — handled in the next batch.`,
|
||||
})
|
||||
}
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
Selected: <b>{selected.size}</b> / {rows.length} unloaded
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Button size="compact-sm" variant="default" disabled={rows.length === 0} onClick={selectAll}>
|
||||
Select All
|
||||
</Button>
|
||||
<Button size="compact-sm" variant="default" disabled={selected.size === 0} onClick={unselectAll}>
|
||||
Unselect All
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="indigo"
|
||||
leftSection={<ClipboardCheck size={14} />}
|
||||
disabled={selected.size === 0}
|
||||
loading={inspectMutation.isPending}
|
||||
onClick={markInspected}
|
||||
>
|
||||
Mark Selected as Inspected
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="lg">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : rows.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="lg" size="sm">
|
||||
No unloaded import items. Items appear here after Auto Unload on an arrived train.
|
||||
</Text>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={2000}>
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected}
|
||||
onChange={() => (allSelected ? unselectAll() : selectAll())}
|
||||
/>
|
||||
</Table.Th>
|
||||
<Table.Th>Booking ID</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>Customer ID</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
<Table.Th>Arrival Time</Table.Th>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Cargo Type</Table.Th>
|
||||
<Table.Th>Weight</Table.Th>
|
||||
<Table.Th>Train Schedule</Table.Th>
|
||||
<Table.Th>Inspection</Table.Th>
|
||||
<Table.Th>Pickup Option</Table.Th>
|
||||
<Table.Th>Last Mile</Table.Th>
|
||||
<Table.Th>Current Status</Table.Th>
|
||||
<Table.Th ta="right">Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ImportUnloadedItem) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
checked={selected.has(r.id)}
|
||||
onChange={() => toggleOne(r.id)}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.bookingId ? `${r.bookingId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{r.customerId ? `${r.customerId.slice(0, 8)}…` : '—'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.customerName ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatDate(r.arrivalTime)}</Table.Td>
|
||||
<Table.Td>{r.containerNumber ?? '—'}</Table.Td>
|
||||
<Table.Td>{r.cargoType ?? '—'}</Table.Td>
|
||||
<Table.Td>{formatNumber(Number(r.weight))}</Table.Td>
|
||||
<Table.Td>{r.trainSchedule ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={r.inspectionStatus === 'PASSED' ? 'green' : 'gray'} variant="light" size="sm">
|
||||
{r.inspectionStatus ?? 'Not inspected'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{r.pickupOption}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={r.lastMileRequested ? 'blue' : 'gray'} variant="light" size="sm">
|
||||
{r.lastMileRequested ? 'Yes' : 'No'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color="indigo" variant="light" size="sm">{r.currentStatus}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Button size="compact-xs" variant="light" color="orange" onClick={() => setInspectId(r.id)}>
|
||||
Inspect / Report
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
|
||||
<InspectionReportModal
|
||||
opened={Boolean(inspectId)}
|
||||
onClose={() => setInspectId(null)}
|
||||
inventoryId={inspectId}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user