import type { Asset, BookedLine, Invoice } from "@/lib/data"; import { ZAR2 } from "@/lib/utils"; import { AlertTriangle, CheckCircle2 } from "lucide-react"; import { MachineCombobox } from "@/components/MachineCombobox"; import { ScrollFade } from "@/components/ScrollFade"; import { SiteBookingSelect, SiteBookingLegend } from "@/components/SiteBookingSelect"; import { InvoiceUploader, AttachedDocument } from "@/components/InvoiceUploader"; export function InvoiceSummary({ invoice, lines, assets, embedded, }: { invoice: Invoice; lines: BookedLine[]; assets: Asset[]; // Inside the expandable invoice row the surrounding card already shows the number and // site, so the header block is dropped to avoid saying the same thing twice. embedded?: boolean; }) { const unbooked = lines.filter((l) => !l.isBooked); // Two different numbers, both worth showing. // ex-VAT = what actually lands in the P&L as income for the machine. // incl-VAT= what the debtor actually pays (ex-VAT + SARS VAT). // Booking is done ex-VAT: VAT is collected for SARS, never income. const sum = (ls: BookedLine[], k: "amount_excl" | "vat") => ls.reduce((s, l) => s + (l[k] || 0), 0); const bookedExVat = sum(lines.filter((l) => l.isBooked), "amount_excl"); const bookedVat = sum(lines.filter((l) => l.isBooked), "vat"); const unbookedExVat = sum(unbooked, "amount_excl"); const unbookedVat = sum(unbooked, "vat"); const period = `${invoice.period_from.slice(0, 10)} → ${invoice.period_to.slice(0, 10)}`; const periodMonth = new Date(invoice.period_to).toLocaleDateString("en-ZA", { month: "long", year: "numeric", timeZone: "UTC", }); return (
{!embedded && (
{invoice.invoice_no} {/* Site is transcribed from the invoice's own description. There is no sites collection yet — this is a plain label until one exists. */} {invoice.site && ( {invoice.site} )} {invoice.debtor}

{invoice.description}

Order {invoice.order_no || "—"} Debtor {invoice.debtor_no || "—"} {period} {invoice.currency || "ZAR"}
{/* The headline is the VAT-inclusive invoice total — what the debtor pays. */}
Invoice total (incl VAT)
{ZAR2(invoice.total_incl)}
{ZAR2(invoice.total_excl)} + {ZAR2(invoice.vat)} VAT {invoice.vat_pct ? ` @ ${invoice.vat_pct}%` : ""}
)} {embedded && (
{invoice.debtor} · {invoice.description}
Order {invoice.order_no || "—"} Debtor {invoice.debtor_no || "—"} {period}
Invoice total (incl VAT)
{ZAR2(invoice.total_incl)}
)} {/* Booking status. An unassigned line is never guessed at — it stays visible and the shortfall is stated, rather than being charged to a wrong machine. */} {unbooked.length > 0 ? (

{unbooked.length} line{unbooked.length > 1 ? "s" : ""} not yet booked to a machine — choose a plant item below.

) : (

All {lines.length} lines booked to {periodMonth} income.

)}
Source document
{invoice.source_pdf ? ( ) : ( )}
{invoice.escalation > 0 && }
); } // Shows the booked money three ways at once, so nobody has to guess which figure the // dashboard is using: // // booked to income (ex VAT) → what lands in the P&L for the machine // VAT → collected for SARS, NOT income // billed to customer → ex-VAT + VAT, what the debtor actually pays function BookingMoney({ bookedExVat, bookedVat, unbookedExVat = 0, unbookedVat = 0, }: { bookedExVat: number; bookedVat: number; unbookedExVat?: number; unbookedVat?: number; }) { return (
{unbookedExVat > 0 && ( <> )}
); } function Item({ label, value, strong }: { label: string; value: number; strong?: boolean }) { return ( {label} {ZAR2(value)} ); } function TotalRow({ label, value, strong }: { label: string; value: number; strong?: boolean }) { return (
{label}
{ZAR2(value || 0)}
); } export function InvoiceTable({ lines, assets }: { lines: BookedLine[]; assets: Asset[] }) { const machines = assets .filter((a) => a.is_active !== false) .map((a) => ({ id: a.id, code: a.code || "", name: a.name, category: a.category || "Other" })); return ( // Rule 1 + 4: the table scrolls inside its own container, so the page itself never // scrolls sideways at 375px. `-webkit-overflow-scrolling` is not a Tailwind utility — // written as a class it is inert — so it is set inline to actually take effect.
{lines.map((l) => ( {/* Sticky first column — keeps the machine identifiable while scrolling. It needs its own opaque background, so it has to repeat the row's unbooked tint; otherwise the flag disappears under the white. */} ))}
Machine Description Qty Unit Rate Hire ex VAT VAT Total incl Booked to
{l.line_no} {l.machine_code}
{l.description} {l.qty} {l.unit} {ZAR2(l.rate)} {ZAR2(l.amount_excl)} {ZAR2(l.vat)} {ZAR2(l.total_incl)}
{(!l.isBooked || l.booked_site) && ( )}
); }