Files
edr-platform/apps/edr-freight-web/backoffice/src/components/bookings/BookingPricingSummary.tsx
Nathnael 78c6f8be0a feat(bookings): show the invoice number in Pricing & payment
The booking's freight invoice is looked up through the existing invoice
list endpoint (source=booking, sourceId matched by search), newest first
so a re-issue supersedes the old number.
2026-08-28 09:17:35 +00:00

138 lines
4.6 KiB
TypeScript

import { Banknote, Receipt } from "lucide-react";
import { Divider, Group, Paper, Stack, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/services/api";
import type { BookingDetail } from "@/types/booking";
import { SectionCard } from "./detail/SectionCard";
import { detailStyles } from "./detail/booking-detail.styles";
export function BookingPricingSummary({ booking }: { booking: BookingDetail }) {
// The booking's own freight invoice: source `booking`, sourceId = booking id
// (which `search` matches). Newest first — a re-issue supersedes the old one.
const invoiceQuery = useQuery(
api.invoices.list.queryOptions({
input: {
filter: {
page: 1,
pageSize: 1,
sources: "booking",
search: booking.id,
sortBy: "createdAt",
sortOrder: "DESC",
},
},
}),
);
const invoiceNumber = invoiceQuery.data?.items[0]?.invoiceNumber ?? null;
const computed = Number(booking.totalAmount);
// The booking price is computed from the contract and is NOT staff-editable.
// A historical `adjustedTotalAmount` (from before adjustments were removed)
// is still shown read-only so old records render correctly.
const isAdjusted =
booking.adjustedTotalAmount !== null &&
booking.adjustedTotalAmount !== undefined;
const effective = isAdjusted ? Number(booking.adjustedTotalAmount) : computed;
const lineItems = booking.pricingBreakdown?.lineItems ?? [];
const fmt = (n: number) =>
`${booking.paymentCurrency} ${n.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
return (
<SectionCard icon={Banknote} title="Pricing & payment">
<Stack gap="md">
<Paper radius="md" withBorder p="md" style={detailStyles.highlightCard}>
<Text size="xs" c="dimmed" fw={500} tt="uppercase" lts="0.04em">
{isAdjusted ? "Adjusted total" : "Total amount"}
</Text>
<Text
size="xl"
fw={700}
c="edr-green.9"
mt={4}
style={{ fontVariantNumeric: "tabular-nums", letterSpacing: "-0.5px" }}
>
{fmt(effective)}
</Text>
{isAdjusted && (
<Text size="xs" c="dimmed" mt={2}>
Computed: {fmt(computed)}
{booking.adjustmentReason ? ` · ${booking.adjustmentReason}` : ""}
</Text>
)}
</Paper>
<Row label="Payment status" value={booking.paymentStatus} />
{invoiceNumber && <Row label="Invoice number" value={invoiceNumber} mono />}
{booking.pnrCode && <Row label="PNR code" value={booking.pnrCode} mono />}
{lineItems.length > 0 && (
<>
<Divider color="var(--mantine-color-gray-2)" />
<Group gap={6}>
<Receipt size={13} color="var(--mantine-color-gray-5)" />
<Text size="xs" c="dimmed" fw={500} tt="uppercase" lts="0.04em">
Price breakdown
</Text>
</Group>
<Stack gap="xs">
{lineItems.map((li, i) => (
<Group
key={`${li.code}-${i}`}
justify="space-between"
px="sm"
py={6}
style={{
borderRadius: 8,
border: "1px solid var(--mantine-color-gray-2)",
background: "var(--mantine-color-gray-0)",
}}
>
<Text size="sm" c="dimmed">
{li.description}
</Text>
<Text size="sm" fw={600} style={{ fontVariantNumeric: "tabular-nums" }}>
{Number(li.amount).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}{" "}
{li.currency}
</Text>
</Group>
))}
</Stack>
</>
)}
</Stack>
</SectionCard>
);
}
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<Group
justify="space-between"
px="sm"
py="xs"
style={{
borderRadius: 8,
border: "1px solid var(--mantine-color-gray-2)",
background: "var(--mantine-color-gray-0)",
}}
>
<Text size="sm" c="dimmed">
{label}
</Text>
<Text size="sm" fw={600} ff={mono ? "monospace" : undefined}>
{value}
</Text>
</Group>
);
}