mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
120 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|