feat(warehouse): expand an inventory row to see which containers ride which truck

The inventory table has one row per inventory item, so on a self-haul booking
with several trucks there was no way to tell which container sat on which
truck without opening a document. The plate column now lists every plate, but
not the mapping.

Each row with a customer truck gets a chevron that opens a per-truck
breakdown: plate, driver, type, the containers it carries, and whether it has
arrived. Bulk trucks show "Bulk" — they haul loose tonnage, not containers.

The breakdown fetches only when opened, so a table nobody expands costs no
extra requests, and it reuses the existing per-booking trucks endpoint rather
than widening the inventory query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-20 12:52:31 +00:00
parent 0c0de284b0
commit 7b777792e3
2 changed files with 163 additions and 3 deletions

View File

@@ -0,0 +1,113 @@
import { useQuery } from "@tanstack/react-query";
import { Badge, Group, Loader, Table, Text } from "@mantine/core";
import { warehouseService } from "@/services/warehouse.service";
/**
* The trucks carrying one booking's cargo, and which containers ride each.
*
* A self-haul booking can have several trucks, each with 12 containers, but
* the inventory table has one row per inventory item — so which container sits
* on which truck was never visible without opening a document. Fetched lazily:
* only an expanded row costs a request.
*/
export function TruckBreakdownRow({
bookingId,
colSpan,
}: {
bookingId: string;
colSpan: number;
}) {
const { data: trucks = [], isLoading } = useQuery({
queryKey: ["booking-customer-trucks", bookingId],
queryFn: () => warehouseService.getCustomerTrucks(bookingId),
});
return (
<Table.Tr>
<Table.Td colSpan={colSpan} bg="var(--mantine-color-gray-0)">
{isLoading ? (
<Group gap="xs" py="xs">
<Loader size="xs" />
<Text size="xs" c="dimmed">
Loading trucks
</Text>
</Group>
) : trucks.length === 0 ? (
<Text size="xs" c="dimmed" py="xs">
No customer trucks assigned to this booking.
</Text>
) : (
<Table verticalSpacing={4} withRowBorders={false}>
<Table.Thead>
<Table.Tr>
<Table.Th>
<Text size="xs" c="dimmed">
Truck
</Text>
</Table.Th>
<Table.Th>
<Text size="xs" c="dimmed">
Driver
</Text>
</Table.Th>
<Table.Th>
<Text size="xs" c="dimmed">
Type
</Text>
</Table.Th>
<Table.Th>
<Text size="xs" c="dimmed">
Containers
</Text>
</Table.Th>
<Table.Th>
<Text size="xs" c="dimmed">
Status
</Text>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{trucks.map((truck) => (
<Table.Tr key={truck.id}>
<Table.Td>
<Text size="xs" fw={600}>
{truck.plateNumber}
</Text>
</Table.Td>
<Table.Td>
<Text size="xs">{truck.driverName}</Text>
</Table.Td>
<Table.Td>
<Text size="xs">{truck.truckType}</Text>
</Table.Td>
<Table.Td>
{/* Bulk trucks carry loose tonnage, not containers. */}
<Text size="xs">
{truck.containers?.length
? truck.containers.map((c) => c.containerNumber).join(", ")
: "Bulk"}
</Text>
</Table.Td>
<Table.Td>
<Badge
size="xs"
radius="sm"
variant="light"
color={truck.arrivedAt ? "edr-green" : "gray"}
>
{truck.arrivedAt
? `Arrived ${new Date(truck.arrivedAt).toLocaleString()}`
: "Not arrived"}
</Badge>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
</Table.Td>
</Table.Tr>
);
}

View File

@@ -1,6 +1,6 @@
import { useState, type MouseEvent } from 'react';
import { Fragment, useState, type MouseEvent } from 'react';
import { ActionIcon, Badge, Button, Checkbox, Group, Table, Text, Tooltip } from '@mantine/core';
import { ArrowRightLeft, ClipboardList, Coins, Download, Eye, FileText, History, MapPin } from 'lucide-react';
import { ArrowRightLeft, ChevronDown, ChevronRight, ClipboardList, Coins, Download, Eye, FileText, History, MapPin } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { warehouseService } from '@/services/warehouse.service';
@@ -10,6 +10,7 @@ import {
type WarehouseInventoryItem,
} from '@/types/warehouse';
import { InventoryStatusBadge } from './badges';
import { TruckBreakdownRow } from './TruckBreakdownRow';
import { extractDownloadErrorMessage, formatDate, formatNumber, humanizeEnum } from './options';
import { openPdfBlob } from './pdf';
@@ -120,6 +121,16 @@ export function WarehouseInventoryTable({
someSelected,
}: WarehouseInventoryTableProps) {
const selectable = Boolean(onToggleSelect);
// Bookings whose truck breakdown is open. Expanded rows fetch on demand, so a
// closed table costs nothing extra.
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const toggleExpanded = (bookingId: string) =>
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(bookingId)) next.delete(bookingId);
else next.add(bookingId);
return next;
});
if (items.length === 0) {
return (
@@ -144,6 +155,8 @@ export function WarehouseInventoryTable({
/>
</Table.Th>
)}
{/* Expander for the per-truck breakdown. */}
<Table.Th w={32} />
<Table.Th>Booking</Table.Th>
<Table.Th>GRN</Table.Th>
<Table.Th>Facility</Table.Th>
@@ -174,8 +187,16 @@ export function WarehouseInventoryTable({
(!item.booking?.tradeDirection || item.booking.tradeDirection === 'IMPORT');
const handoverReference = handoverDocumentReference(item);
// Only offer the breakdown where there is one: a plate means at
// least one customer truck is on the booking.
const hasCustomerTrucks = Boolean(
item.bookingId && item.booking?.customerTruckPlateNumber?.trim(),
);
const isExpanded = Boolean(item.bookingId && expanded.has(item.bookingId));
return (
<Table.Tr key={item.id}>
<Fragment key={item.id}>
<Table.Tr>
{selectable && (
<Table.Td>
<Checkbox
@@ -185,6 +206,23 @@ export function WarehouseInventoryTable({
/>
</Table.Td>
)}
<Table.Td>
{hasCustomerTrucks ? (
<Tooltip
label={isExpanded ? 'Hide trucks' : 'Show which containers ride which truck'}
withArrow
>
<ActionIcon
variant="subtle"
size="sm"
aria-label={isExpanded ? 'Hide trucks' : 'Show trucks'}
onClick={() => toggleExpanded(item.bookingId as string)}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</ActionIcon>
</Tooltip>
) : null}
</Table.Td>
<Table.Td>
{item.bookingReference || item.booking?.reference || item.bookingId ? (
<Tooltip label={item.bookingId ?? ''} withArrow disabled={!item.bookingId}>
@@ -309,6 +347,15 @@ export function WarehouseInventoryTable({
</Group>
</Table.Td>
</Table.Tr>
{isExpanded && item.bookingId ? (
<TruckBreakdownRow
bookingId={item.bookingId}
// Expander + every data column + actions, plus the checkbox
// when the table is selectable.
colSpan={selectable ? 14 : 13}
/>
) : null}
</Fragment>
);
})}
</Table.Tbody>