Files
edr-platform/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingTrucksPanel.tsx
2026-08-08 20:45:05 +00:00

120 lines
4.3 KiB
TypeScript

import { useMemo, useState } from "react";
import { useQueries, useQuery } from "@tanstack/react-query";
import { Button, Center, Group, Loader, SimpleGrid, Stack, Table, Text } from "@mantine/core";
import { Coins, Truck } from "lucide-react";
import { api } from "@/services/api";
import { FeePreviewModal } from "@/components/warehouses/FeePreviewModal";
import { groupByBooking, TruckRows } from "@/pages/warehouses/ImportTrucksPage";
import { SectionCard } from "./SectionCard";
import { MetricTile } from "./MetricTile";
const money = (amount: number, currency: string) =>
`${Number(amount).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} ${currency === "ETB" ? "Birr (ETB)" : currency}`;
/**
* Cargo costs (booking-level totals) plus the same truck-import block the
* Unloaded Queue's "Import trucks" view uses — Truck Arrival/Leaving, Exit
* paper, Handover, Inspect, Detention times, Warehouse gate times — reused
* as-is so this tab never drifts from that queue's behavior.
*/
export function BookingTrucksPanel({ bookingId }: { bookingId: string }) {
const [feeModalOpen, setFeeModalOpen] = useState(false);
const inventoryQuery = useQuery(
api.warehouses.listInventory.queryOptions({ input: { filter: { bookingId } } }),
);
const inventoryItems = inventoryQuery.data ?? [];
const latestInventory = inventoryItems[0] ?? null;
// Same query key as the Unloaded Queue page — shares its cache instead of
// refetching the whole queue when it's already loaded elsewhere.
const unloadedQuery = useQuery(api.warehouses.importUnloadedQueue.queryOptions({}));
const bookingRows = useMemo(
() => (unloadedQuery.data ?? []).filter((row) => row.bookingId === bookingId),
[unloadedQuery.data, bookingId],
);
const group = useMemo(() => {
const [existing] = groupByBooking(bookingRows);
return (
existing ?? {
bookingId,
bookingReference: bookingId,
customerName: null,
trainSchedule: null,
status: "NONE",
arrivalTime: null,
rows: [],
}
);
}, [bookingRows, bookingId]);
// Booking-level cost strip: same per-row fee preview the accrual dashboard
// and FeePreviewModal already use, summed across every inventory row on
// this booking rather than duplicated per row.
const feeQueries = useQueries({
queries: inventoryItems.map((item) =>
api.warehouses.feePreview.queryOptions({ input: { inventoryId: item.id, billingCurrency: "USD" } }),
),
});
const allFees = feeQueries.flatMap((q) => q.data ?? []);
const feeCurrency = allFees[0]?.currency ?? "USD";
const sumByType = (type: string) =>
allFees.filter((f) => f.ruleType === type).reduce((sum, f) => sum + Number(f.amount || 0), 0);
if (inventoryQuery.isLoading || unloadedQuery.isLoading) {
return (
<Center py={60}>
<Group gap={10}>
<Loader color="edr-green" />
<Text c="dimmed">Loading trucks</Text>
</Group>
</Center>
);
}
return (
<Stack gap="lg">
<SectionCard
icon={Coins}
title="Cargo costs"
subtitle="Storage, demurrage & double handling — booking total"
accent="teal"
extra={
latestInventory && (
<Button size="xs" variant="light" onClick={() => setFeeModalOpen(true)}>
View breakdown
</Button>
)
}
>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
<MetricTile label="Storage" value={money(sumByType("STORAGE_FEE"), feeCurrency)} />
<MetricTile label="Demurrage" value={money(sumByType("DEMURRAGE_FEE"), feeCurrency)} />
<MetricTile label="Double handling" value={money(sumByType("DOUBLE_HANDLING_FEE"), feeCurrency)} />
</SimpleGrid>
</SectionCard>
<SectionCard icon={Truck} title="Trucks" accent="grape">
<Table.ScrollContainer minWidth={1100}>
<Table verticalSpacing="xs" fz="xs">
<Table.Tbody>
<TruckRows group={group} />
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</SectionCard>
<FeePreviewModal
opened={feeModalOpen}
onClose={() => setFeeModalOpen(false)}
inventoryId={latestInventory?.id ?? null}
/>
</Stack>
);
}