mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 20:05:41 +00:00
implemented import truck page
This commit is contained in:
@@ -121,6 +121,7 @@ import ArrivalQueuePage from "./pages/warehouses/ArrivalQueuePage";
|
|||||||
import DispatchQueuePage from "./pages/warehouses/DispatchQueuePage";
|
import DispatchQueuePage from "./pages/warehouses/DispatchQueuePage";
|
||||||
import ExportDjiboutiUnloadingQueuePage from "./pages/warehouses/ExportDjiboutiUnloadingQueuePage";
|
import ExportDjiboutiUnloadingQueuePage from "./pages/warehouses/ExportDjiboutiUnloadingQueuePage";
|
||||||
import ExportWarehouseFlowPage from "./pages/warehouses/ExportWarehouseFlowPage";
|
import ExportWarehouseFlowPage from "./pages/warehouses/ExportWarehouseFlowPage";
|
||||||
|
import ImportTrucksPage from "./pages/warehouses/ImportTrucksPage";
|
||||||
import ImportWarehouseFlowPage from "./pages/warehouses/ImportWarehouseFlowPage";
|
import ImportWarehouseFlowPage from "./pages/warehouses/ImportWarehouseFlowPage";
|
||||||
import InterchangeDocumentsPage from "./pages/warehouses/InterchangeDocumentsPage";
|
import InterchangeDocumentsPage from "./pages/warehouses/InterchangeDocumentsPage";
|
||||||
import InventoryInquiryPage from "./pages/warehouses/InventoryInquiryPage";
|
import InventoryInquiryPage from "./pages/warehouses/InventoryInquiryPage";
|
||||||
@@ -404,6 +405,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
icon: <PackageOpen />,
|
icon: <PackageOpen />,
|
||||||
permission: FREIGHT_PERMS.warehouseInventory.view,
|
permission: FREIGHT_PERMS.warehouseInventory.view,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Import Trucks",
|
||||||
|
href: "/dashboard/import-trucks",
|
||||||
|
icon: <Truck />,
|
||||||
|
permission: FREIGHT_PERMS.warehouseInventory.view,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Dispatch Queue",
|
label: "Dispatch Queue",
|
||||||
href: "/dashboard/dispatch-queue",
|
href: "/dashboard/dispatch-queue",
|
||||||
@@ -1052,6 +1059,7 @@ const App = () => {
|
|||||||
<Route path="loading-queue" element={<LoadingQueuePage />} />
|
<Route path="loading-queue" element={<LoadingQueuePage />} />
|
||||||
<Route path="intercity" element={<IntercityPage />} />
|
<Route path="intercity" element={<IntercityPage />} />
|
||||||
<Route path="trucks-on-site" element={<TrucksOnSitePage />} />
|
<Route path="trucks-on-site" element={<TrucksOnSitePage />} />
|
||||||
|
<Route path="import-trucks" element={<ImportTrucksPage />} />
|
||||||
<Route path="loaded-inventory" element={<LoadedInventoryPage />} />
|
<Route path="loaded-inventory" element={<LoadedInventoryPage />} />
|
||||||
<Route path="dispatch-queue" element={<DispatchQueuePage />} />
|
<Route path="dispatch-queue" element={<DispatchQueuePage />} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -0,0 +1,495 @@
|
|||||||
|
import { Fragment, useMemo, useState } from "react";
|
||||||
|
import { useQueries, useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Alert,
|
||||||
|
Badge,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Menu,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
ClipboardCheck,
|
||||||
|
FileText,
|
||||||
|
MoreHorizontal,
|
||||||
|
Receipt,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { Freight } from "@edr/types";
|
||||||
|
|
||||||
|
import { PageContainer, PageHeader } from "@/components/page";
|
||||||
|
import ListControls from "@/components/common/ListControls";
|
||||||
|
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||||
|
import { InspectionReportModal } from "@/components/warehouses/InspectionReportModal";
|
||||||
|
import { TruckDetentionModal } from "@/components/operations/TruckDetentionModal";
|
||||||
|
import { extractDownloadErrorMessage, formatNumber } from "@/components/warehouses/options";
|
||||||
|
import { openPdfBlob } from "@/components/warehouses/pdf";
|
||||||
|
import { useListControls } from "@/hooks/useListControls";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { api } from "@/services/api";
|
||||||
|
import { lastMileService } from "@/services/last-mile.service";
|
||||||
|
import { warehouseService, type LastMileArrivalTruck } from "@/services/warehouse.service";
|
||||||
|
import type { ImportUnloadedItem } from "@/types/warehouse";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import trucks — the unloaded queue seen truck-first instead of item-first.
|
||||||
|
*
|
||||||
|
* The unloaded-queue tab lists inventory rows; the gate and the billing desk
|
||||||
|
* ask "which truck takes this booking out, and what does it owe". So bookings
|
||||||
|
* are the collapsed row and their trucks are the detail, each carrying its own
|
||||||
|
* cargo costs (demurrage/storage, matched by container) and — EDR fleet only —
|
||||||
|
* its detention clock. Customer self-haul shows "—" for detention: EDR bills
|
||||||
|
* detention on its own trucks only.
|
||||||
|
*
|
||||||
|
* Per-booking truck/fee/detention queries run only while a booking is expanded;
|
||||||
|
* the queue can hold hundreds of bookings and fetching all of them up front
|
||||||
|
* would be several hundred requests for rows nobody opened.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const COLS = 9;
|
||||||
|
|
||||||
|
const money = (amount: number, currency: string) =>
|
||||||
|
`${Number(amount).toLocaleString(undefined, { maximumFractionDigits: 2 })} ${currency === "ETB" ? "ETB" : currency}`;
|
||||||
|
|
||||||
|
interface BookingGroup {
|
||||||
|
bookingId: string;
|
||||||
|
bookingReference: string;
|
||||||
|
customerName: string | null;
|
||||||
|
trainSchedule: string | null;
|
||||||
|
status: string;
|
||||||
|
arrivalTime: string | null;
|
||||||
|
rows: ImportUnloadedItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One collapsed line per booking; its inventory rows travel with it for the fee lookup. */
|
||||||
|
function groupByBooking(items: ImportUnloadedItem[]): BookingGroup[] {
|
||||||
|
const groups = new Map<string, BookingGroup>();
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.bookingId) continue;
|
||||||
|
const existing = groups.get(item.bookingId);
|
||||||
|
if (existing) {
|
||||||
|
existing.rows.push(item);
|
||||||
|
// Mixed statuses across a booking's rows are normal mid-pickup — show the
|
||||||
|
// least-advanced one so the row reads as "still has work on it".
|
||||||
|
if (existing.status !== item.currentStatus) existing.status = "MIXED";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
groups.set(item.bookingId, {
|
||||||
|
bookingId: item.bookingId,
|
||||||
|
bookingReference: item.bookingReference ?? item.bookingId,
|
||||||
|
customerName: item.customerName,
|
||||||
|
trainSchedule: item.trainSchedule,
|
||||||
|
status: item.currentStatus,
|
||||||
|
arrivalTime: item.arrivalTime,
|
||||||
|
rows: [item],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [...groups.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TruckRow {
|
||||||
|
key: string;
|
||||||
|
plate: string;
|
||||||
|
driver: string | null;
|
||||||
|
truckType: string | null;
|
||||||
|
containers: string[];
|
||||||
|
/** Inventory rows this truck carries — the ids the documents and fees hang off. */
|
||||||
|
inventoryIds: string[];
|
||||||
|
weight: number;
|
||||||
|
demurrage: number;
|
||||||
|
storage: number;
|
||||||
|
vehicleId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haulage mode is decided by which list comes back non-empty — a booking is
|
||||||
|
* either EDR last mile or customer self-haul, never both.
|
||||||
|
*/
|
||||||
|
function TruckRows({ group }: { group: BookingGroup }) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [inspectId, setInspectId] = useState<string | null>(null);
|
||||||
|
const [detentionOpen, setDetentionOpen] = useState(false);
|
||||||
|
|
||||||
|
const edrQuery = useQuery({
|
||||||
|
queryKey: ["booking-edr-trucks", group.bookingId],
|
||||||
|
queryFn: () => warehouseService.getLastMileTrucks(group.bookingId),
|
||||||
|
});
|
||||||
|
const edrTrucks = edrQuery.data ?? [];
|
||||||
|
|
||||||
|
const customerQuery = useQuery({
|
||||||
|
queryKey: ["booking-customer-trucks", group.bookingId],
|
||||||
|
queryFn: () => warehouseService.getCustomerTrucks(group.bookingId),
|
||||||
|
enabled: edrQuery.isSuccess && edrTrucks.length === 0,
|
||||||
|
});
|
||||||
|
const customerTrucks = customerQuery.data ?? [];
|
||||||
|
const isEdr = edrTrucks.length > 0;
|
||||||
|
|
||||||
|
const lastMileId = edrTrucks[0]?.lastMileId ?? null;
|
||||||
|
|
||||||
|
const detentionQuery = useQuery({
|
||||||
|
queryKey: ["truck-detention-preview-import-trucks", lastMileId],
|
||||||
|
queryFn: () => lastMileService.truckDetentionPreview(lastMileId as string).then((r) => r.data),
|
||||||
|
enabled: Boolean(lastMileId),
|
||||||
|
});
|
||||||
|
const detentionByVehicle = new Map(
|
||||||
|
(detentionQuery.data?.groups ?? []).map((g) => [g.vehicleId ?? "", g]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastMileRecordQuery = useQuery({
|
||||||
|
queryKey: ["last-mile-record-import-trucks", lastMileId],
|
||||||
|
queryFn: () => lastMileService.getById(lastMileId as string).then((r) => r.data),
|
||||||
|
enabled: detentionOpen && Boolean(lastMileId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Same per-inventory-row preview the accrual dashboard bills off; the queue
|
||||||
|
// rows already ARE this booking's import inventory, so no second list fetch.
|
||||||
|
const feeQueries = useQueries({
|
||||||
|
queries: group.rows.map((row) =>
|
||||||
|
api.warehouses.feePreview.queryOptions({
|
||||||
|
input: { inventoryId: row.id, billingCurrency: "USD" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const feesByInventory = new Map(group.rows.map((row, i) => [row.id, feeQueries[i]?.data ?? []]));
|
||||||
|
const feeCurrency = feeQueries.flatMap((q) => q.data ?? [])[0]?.currency ?? "USD";
|
||||||
|
const rowByContainer = new Map(
|
||||||
|
group.rows.filter((r) => r.containerNumber).map((r) => [r.containerNumber as string, r]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Fees follow the container onto the truck; a bulk truck carries the whole booking. */
|
||||||
|
const costsFor = (containers: string[]) => {
|
||||||
|
const matched = containers.map((c) => rowByContainer.get(c)).filter(Boolean) as ImportUnloadedItem[];
|
||||||
|
const rows = matched.length > 0 ? matched : group.rows;
|
||||||
|
const fees = rows.flatMap((r) => feesByInventory.get(r.id) ?? []);
|
||||||
|
const sum = (type: string) =>
|
||||||
|
fees.filter((f) => f.ruleType === type).reduce((total, f) => total + Number(f.amount || 0), 0);
|
||||||
|
return {
|
||||||
|
inventoryIds: rows.map((r) => r.id),
|
||||||
|
weight: rows.reduce((total, r) => total + (Number(r.weight) || 0), 0),
|
||||||
|
demurrage: sum("DEMURRAGE_FEE"),
|
||||||
|
storage: sum("STORAGE_FEE"),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromEdr = (t: LastMileArrivalTruck): TruckRow => {
|
||||||
|
const containers = t.containerNumber ? [t.containerNumber] : [];
|
||||||
|
return {
|
||||||
|
key: t.vehicleId,
|
||||||
|
plate: [t.truckPlateNumber, t.trailerPlateNumber].filter(Boolean).join(" + ") || "—",
|
||||||
|
driver: t.driverName,
|
||||||
|
truckType: t.truckType,
|
||||||
|
containers,
|
||||||
|
vehicleId: t.vehicleId,
|
||||||
|
...costsFor(containers),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const fromCustomer = (t: Freight.ICustomerTruck): TruckRow => {
|
||||||
|
const containers = (t.containers ?? []).map((c) => c.containerNumber);
|
||||||
|
return {
|
||||||
|
key: t.id,
|
||||||
|
plate: t.plateNumber,
|
||||||
|
driver: t.driverName,
|
||||||
|
truckType: t.truckType,
|
||||||
|
containers,
|
||||||
|
vehicleId: null,
|
||||||
|
...costsFor(containers),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const trucks: TruckRow[] = isEdr ? edrTrucks.map(fromEdr) : customerTrucks.map(fromCustomer);
|
||||||
|
|
||||||
|
const openDocument = async (
|
||||||
|
kind: "release" | "handover",
|
||||||
|
inventoryId: string,
|
||||||
|
label: string,
|
||||||
|
) => {
|
||||||
|
setBusy(true);
|
||||||
|
const pdfWindow = window.open("", "_blank");
|
||||||
|
try {
|
||||||
|
const response =
|
||||||
|
kind === "release"
|
||||||
|
? await warehouseService.downloadReleaseDocument(inventoryId)
|
||||||
|
: await warehouseService.downloadHandoverDocument(inventoryId);
|
||||||
|
openPdfBlob(response.data, `${kind}-${group.bookingReference}.pdf`, pdfWindow);
|
||||||
|
} catch (error) {
|
||||||
|
pdfWindow?.close();
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: `${label} failed`,
|
||||||
|
description: await extractDownloadErrorMessage(error),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (edrQuery.isLoading || (customerQuery.isFetching && customerTrucks.length === 0)) {
|
||||||
|
return (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={COLS}>
|
||||||
|
<Group gap="xs" justify="center" py="xs">
|
||||||
|
<Loader size="xs" />
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Loading trucks…
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trucks.length === 0) {
|
||||||
|
return (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={COLS}>
|
||||||
|
<Text size="xs" c="dimmed" ta="center" py="xs">
|
||||||
|
No truck assigned to this booking yet.
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{trucks.map((t, idx) => {
|
||||||
|
const detention = t.vehicleId ? detentionByVehicle.get(t.vehicleId) : undefined;
|
||||||
|
const primaryId = t.inventoryIds[0] ?? null;
|
||||||
|
return (
|
||||||
|
<Table.Tr key={t.key} bg="var(--mantine-color-gray-0)">
|
||||||
|
<Table.Td />
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{t.plate}
|
||||||
|
</Text>
|
||||||
|
{t.driver && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t.driver}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge size="sm" radius="sm" variant="light" color={isEdr ? "edr-green" : "blue"}>
|
||||||
|
{isEdr ? "EDR" : "Customer"}
|
||||||
|
</Badge>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{t.truckType ?? "—"}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text size="sm">{t.containers.length ? t.containers.join(", ") : "Bulk"}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{formatNumber(t.weight)}</Table.Td>
|
||||||
|
<Table.Td>{money(t.demurrage, feeCurrency)}</Table.Td>
|
||||||
|
<Table.Td>{money(t.storage, feeCurrency)}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{!isEdr || !detention ? (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Group gap={4} wrap="nowrap">
|
||||||
|
<Text size="sm">
|
||||||
|
{detention.chargeableDays ?? 0}d ·{" "}
|
||||||
|
{money(Number(detention.amount ?? 0), detentionQuery.data?.currency ?? "USD")}
|
||||||
|
</Text>
|
||||||
|
{detention.endIsOpen && (
|
||||||
|
<Badge size="xs" color="orange" variant="light">
|
||||||
|
open
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!detention.ruleId && (
|
||||||
|
<Tooltip label="No detention rule matched this truck" withArrow>
|
||||||
|
<Text size="xs" c="red">
|
||||||
|
no rule
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td ta="right">
|
||||||
|
<Menu shadow="md" width={220} position="bottom-end" withinPortal>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon variant="subtle" color="gray" aria-label="Truck actions" loading={busy}>
|
||||||
|
<MoreHorizontal size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<FileText size={14} />}
|
||||||
|
disabled={!primaryId}
|
||||||
|
onClick={() => primaryId && openDocument("release", primaryId, "Exit paper")}
|
||||||
|
>
|
||||||
|
Exit paper
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<FileText size={14} />}
|
||||||
|
disabled={!primaryId}
|
||||||
|
onClick={() => primaryId && openDocument("handover", primaryId, "Handover")}
|
||||||
|
>
|
||||||
|
Handover
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<ClipboardCheck size={14} />}
|
||||||
|
disabled={!primaryId}
|
||||||
|
onClick={() => setInspectId(primaryId)}
|
||||||
|
>
|
||||||
|
Inspect / report
|
||||||
|
</Menu.Item>
|
||||||
|
{isEdr && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<Receipt size={14} />}
|
||||||
|
disabled={!lastMileId}
|
||||||
|
onClick={() => setDetentionOpen(true)}
|
||||||
|
>
|
||||||
|
Detention times…
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
{/* Modals portal out of the table, so one mount for the whole
|
||||||
|
booking hangs off the first truck's cell. */}
|
||||||
|
{idx === 0 && (
|
||||||
|
<>
|
||||||
|
<InspectionReportModal
|
||||||
|
opened={Boolean(inspectId)}
|
||||||
|
onClose={() => setInspectId(null)}
|
||||||
|
inventoryId={inspectId}
|
||||||
|
/>
|
||||||
|
{isEdr && (
|
||||||
|
<TruckDetentionModal
|
||||||
|
opened={detentionOpen}
|
||||||
|
onClose={() => setDetentionOpen(false)}
|
||||||
|
record={lastMileRecordQuery.data ?? null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportTrucksPage() {
|
||||||
|
const { data: items = [], isLoading } = useQuery(
|
||||||
|
api.warehouses.importUnloadedQueue.queryOptions({}),
|
||||||
|
);
|
||||||
|
const [expanded, setExpanded] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const groups = useMemo(() => groupByBooking(items), [items]);
|
||||||
|
const controls = useListControls(groups, {
|
||||||
|
searchKeys: ["bookingReference", "customerName", "trainSchedule"],
|
||||||
|
dateKey: "arrivalTime",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader
|
||||||
|
title="Import trucks"
|
||||||
|
subtitle="Unloaded import bookings and the trucks taking them out — cargo costs per truck, detention on EDR fleet."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ListControls
|
||||||
|
search={controls.search}
|
||||||
|
onSearchChange={controls.setSearch}
|
||||||
|
searchPlaceholder="Booking, customer, train…"
|
||||||
|
dateFrom={controls.dateFrom}
|
||||||
|
onDateFromChange={controls.setDateFrom}
|
||||||
|
dateTo={controls.dateTo}
|
||||||
|
onDateToChange={controls.setDateTo}
|
||||||
|
dateLabel="Arrived"
|
||||||
|
hasFilters={controls.hasFilters}
|
||||||
|
onReset={controls.reset}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Group justify="center" py="lg">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Group>
|
||||||
|
) : controls.pagedRows.length === 0 ? (
|
||||||
|
<Alert variant="light" color="gray">
|
||||||
|
No unloaded import bookings. They appear here after Auto Unload on an arrived train.
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table.ScrollContainer minWidth={1100}>
|
||||||
|
<Table highlightOnHover verticalSpacing="xs">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th w={40} />
|
||||||
|
<Table.Th>Plate</Table.Th>
|
||||||
|
<Table.Th>Type</Table.Th>
|
||||||
|
<Table.Th>Containers</Table.Th>
|
||||||
|
<Table.Th>Weight</Table.Th>
|
||||||
|
<Table.Th>Demurrage</Table.Th>
|
||||||
|
<Table.Th>Storage</Table.Th>
|
||||||
|
<Table.Th>Detention</Table.Th>
|
||||||
|
<Table.Th ta="right">Actions</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{controls.pagedRows.map((g) => {
|
||||||
|
const isOpen = expanded === g.bookingId;
|
||||||
|
return (
|
||||||
|
<Fragment key={g.bookingId}>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
aria-label={isOpen ? "Hide trucks" : "Show trucks"}
|
||||||
|
onClick={() => setExpanded(isOpen ? null : g.bookingId)}
|
||||||
|
>
|
||||||
|
{isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td colSpan={COLS - 1}>
|
||||||
|
<Group gap="md" wrap="wrap">
|
||||||
|
<Text size="sm" fw={700}>
|
||||||
|
{g.bookingReference}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{g.customerName ?? "—"}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Train: {g.trainSchedule ?? "—"}
|
||||||
|
</Text>
|
||||||
|
<Badge size="sm" radius="sm" variant="light" color="gray">
|
||||||
|
{g.status.replace(/_/g, " ")}
|
||||||
|
</Badge>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{g.rows.length} item{g.rows.length === 1 ? "" : "s"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
{isOpen && <TruckRows group={g} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Table.ScrollContainer>
|
||||||
|
<RuleEngineListFooter
|
||||||
|
pagination={controls.pagination}
|
||||||
|
pageCount={controls.pageCount}
|
||||||
|
totalCount={controls.totalCount}
|
||||||
|
itemLabel="bookings"
|
||||||
|
onPaginationChange={controls.setPagination}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user