mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
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:
@@ -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 1–2 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user