mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 21:45:03 +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 { 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 { useToast } from '@/hooks/use-toast';
|
||||||
import { warehouseService } from '@/services/warehouse.service';
|
import { warehouseService } from '@/services/warehouse.service';
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type WarehouseInventoryItem,
|
type WarehouseInventoryItem,
|
||||||
} from '@/types/warehouse';
|
} from '@/types/warehouse';
|
||||||
import { InventoryStatusBadge } from './badges';
|
import { InventoryStatusBadge } from './badges';
|
||||||
|
import { TruckBreakdownRow } from './TruckBreakdownRow';
|
||||||
import { extractDownloadErrorMessage, formatDate, formatNumber, humanizeEnum } from './options';
|
import { extractDownloadErrorMessage, formatDate, formatNumber, humanizeEnum } from './options';
|
||||||
import { openPdfBlob } from './pdf';
|
import { openPdfBlob } from './pdf';
|
||||||
|
|
||||||
@@ -120,6 +121,16 @@ export function WarehouseInventoryTable({
|
|||||||
someSelected,
|
someSelected,
|
||||||
}: WarehouseInventoryTableProps) {
|
}: WarehouseInventoryTableProps) {
|
||||||
const selectable = Boolean(onToggleSelect);
|
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) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -144,6 +155,8 @@ export function WarehouseInventoryTable({
|
|||||||
/>
|
/>
|
||||||
</Table.Th>
|
</Table.Th>
|
||||||
)}
|
)}
|
||||||
|
{/* Expander for the per-truck breakdown. */}
|
||||||
|
<Table.Th w={32} />
|
||||||
<Table.Th>Booking</Table.Th>
|
<Table.Th>Booking</Table.Th>
|
||||||
<Table.Th>GRN</Table.Th>
|
<Table.Th>GRN</Table.Th>
|
||||||
<Table.Th>Facility</Table.Th>
|
<Table.Th>Facility</Table.Th>
|
||||||
@@ -174,8 +187,16 @@ export function WarehouseInventoryTable({
|
|||||||
(!item.booking?.tradeDirection || item.booking.tradeDirection === 'IMPORT');
|
(!item.booking?.tradeDirection || item.booking.tradeDirection === 'IMPORT');
|
||||||
const handoverReference = handoverDocumentReference(item);
|
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 (
|
return (
|
||||||
<Table.Tr key={item.id}>
|
<Fragment key={item.id}>
|
||||||
|
<Table.Tr>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -185,6 +206,23 @@ export function WarehouseInventoryTable({
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</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>
|
<Table.Td>
|
||||||
{item.bookingReference || item.booking?.reference || item.bookingId ? (
|
{item.bookingReference || item.booking?.reference || item.bookingId ? (
|
||||||
<Tooltip label={item.bookingId ?? ''} withArrow disabled={!item.bookingId}>
|
<Tooltip label={item.bookingId ?? ''} withArrow disabled={!item.bookingId}>
|
||||||
@@ -309,6 +347,15 @@ export function WarehouseInventoryTable({
|
|||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</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>
|
</Table.Tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user