mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(warehouses): expandable booking rows list containers / cargo inline
Every booking row in the queue tables (export received, ready-to-load,
loaded/dispatch, import unloaded) gets a chevron that expands to the
booking's items: container number, goods, lifecycle stage badge
(PENDING..DELIVERED), carrying truck, and GRN.
Bulk bookings (no container units) show a one-line summary instead
("Bulk cargo — Coffee export cargo, 3,200 t"). The expansion reuses the
['container-items', bookingId] query the container modal already uses, so
data is shared and instant after either has loaded it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -257,6 +257,83 @@ const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayl
|
||||
|
||||
|
||||
|
||||
|
||||
const SUB_STAGE_COLOR: Record<string, string> = {
|
||||
PENDING: 'gray',
|
||||
RECEIVED: 'blue',
|
||||
GRN: 'teal',
|
||||
ASSIGNED: 'indigo',
|
||||
LOADED: 'grape',
|
||||
LEFT: 'orange',
|
||||
DELIVERED: 'green',
|
||||
};
|
||||
|
||||
/**
|
||||
* Expanded booking row: the booking's containers / bulk items with their
|
||||
* lifecycle stage. Shares the ['container-items', bookingId] cache with
|
||||
* ContainerItemsModal, so expanding after using the modal is instant.
|
||||
*/
|
||||
function BookingItemsExpansion({
|
||||
bookingId,
|
||||
colSpan,
|
||||
bulkFallback,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
colSpan: number;
|
||||
bulkFallback?: string;
|
||||
}) {
|
||||
const { data: items = [], isLoading } = useQuery({
|
||||
queryKey: ['container-items', bookingId],
|
||||
queryFn: () => warehouseService.getContainerItems(bookingId as string),
|
||||
enabled: Boolean(bookingId),
|
||||
});
|
||||
|
||||
return (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={colSpan} bg="var(--mantine-color-gray-0)">
|
||||
{isLoading ? (
|
||||
<Group justify="center" py="sm">
|
||||
<Loader size="xs" />
|
||||
</Group>
|
||||
) : items.length === 0 ? (
|
||||
<Text size="xs" c="dimmed" py={6}>
|
||||
{bulkFallback ?? 'No container units recorded on this booking.'}
|
||||
</Text>
|
||||
) : (
|
||||
<Table verticalSpacing={4} fz="xs" withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Container #</Table.Th>
|
||||
<Table.Th>Goods</Table.Th>
|
||||
<Table.Th>Stage</Table.Th>
|
||||
<Table.Th>Truck</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{items.map((i) => (
|
||||
<Table.Tr key={i.containerNumber}>
|
||||
<Table.Td>
|
||||
<Text size="xs" fw={600}>{i.containerNumber}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{i.goods ?? '—'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="xs" variant="light" color={SUB_STAGE_COLOR[i.stage] ?? 'gray'}>
|
||||
{i.stage}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{i.truckPlate ?? '—'}</Table.Td>
|
||||
<Table.Td>{i.grnNumber ?? '—'}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
}
|
||||
|
||||
type ConfirmAction = { title: string; message: string; confirmLabel: string; run: () => void };
|
||||
|
||||
/** One-click bulk actions are irreversible — make the click deliberate. */
|
||||
@@ -1295,6 +1372,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||
|
||||
const pendingRows = rows.filter((r) => r.inspectionStatus !== 'PASSED');
|
||||
@@ -1365,6 +1443,7 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
@@ -1389,7 +1468,18 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
{rows.map((r: ReadyToLoadRow) => {
|
||||
const selectable = r.inspectionStatus !== 'PASSED';
|
||||
return (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
@@ -1425,6 +1515,14 @@ function ExportReceivedTab({ enabled, onChanged }: { enabled: boolean; onChanged
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={18}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
@@ -1450,6 +1548,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
);
|
||||
const qc = useQueryClient();
|
||||
const [trainPickerOpen, setTrainPickerOpen] = useState(false);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null);
|
||||
// Loading is always onto a SPECIFIC pre-dispatch train. No train -> no auto-load.
|
||||
const { data: trains = [], isLoading: trainsLoading } = useQuery({
|
||||
@@ -1571,6 +1670,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
@@ -1584,7 +1684,18 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ReadyToLoadRow) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
@@ -1607,6 +1718,14 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
<InventoryStatusBadge status={r.status as InventoryStatus} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={11}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
@@ -1634,6 +1753,7 @@ function LoadedExportTab({
|
||||
api.warehouses.loadedExport.queryOptions({ enabled }),
|
||||
);
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const bulkDispatch = useMutation(
|
||||
api.warehouses.bulkDispatchExport.mutationOptions(),
|
||||
);
|
||||
@@ -1743,6 +1863,7 @@ function LoadedExportTab({
|
||||
/>
|
||||
</Table.Th>
|
||||
)}
|
||||
<Table.Th w={34} />
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
<Table.Th>Customer Name</Table.Th>
|
||||
@@ -1755,7 +1876,8 @@ function LoadedExportTab({
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ReadyToLoadRow) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
{dispatchable && (
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
@@ -1765,6 +1887,16 @@ function LoadedExportTab({
|
||||
/>
|
||||
</Table.Td>
|
||||
)}
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={600}>{r.bookingReference ?? '—'}</Text>
|
||||
</Table.Td>
|
||||
@@ -1782,6 +1914,14 @@ function LoadedExportTab({
|
||||
<InventoryStatusBadge status={r.status as InventoryStatus} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={11}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
@@ -2221,6 +2361,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
const readyMutation = useMutation(api.warehouses.markReadyForPickup.mutationOptions());
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [viewItem, setViewItem] = useState<WarehouseInventoryItem | null>(null);
|
||||
@@ -2383,6 +2524,7 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
@@ -2408,7 +2550,18 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((r: ImportUnloadedItem) => (
|
||||
<Table.Tr key={r.id}>
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Show containers"
|
||||
onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
>
|
||||
{expandedRow === r.id ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
@@ -2534,6 +2687,14 @@ function ImportUnloadedQueueTab({ enabled }: { enabled: boolean }) {
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{expandedRow === r.id && (
|
||||
<BookingItemsExpansion
|
||||
bookingId={r.bookingId}
|
||||
colSpan={16}
|
||||
bulkFallback={r.cargoType ? `Bulk cargo — ${r.cargoType}, ${formatNumber(Number(r.weight))} t` : undefined}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
Reference in New Issue
Block a user