"use client"; import { useState, Fragment } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Card, CardHeader } from "@/components/ui/card"; import type { InvoiceDetail, InvoiceStatus } from "@/types"; interface Props { detail: InvoiceDetail; } /** * Renders the invoice header (status, totals, action bar) then * line items grouped by tenant, then billing snapshot. Actions are * mark-paid (POST), delete (DELETE), PDF download (link to /pdf). * * On successful action we router.refresh() — the server-side page * re-renders against the new DB state. For delete we navigate * away first. */ export function InvoiceDetailView({ detail }: Props) { const t = useTranslations("adminBilling"); const router = useRouter(); const { invoice, lines } = detail; const [busyAction, setBusyAction] = useState( null ); const [actionError, setActionError] = useState(""); const [noteInput, setNoteInput] = useState(""); const [noteOpen, setNoteOpen] = useState(false); const markPaid = async () => { setActionError(""); setBusyAction("mark-paid"); try { const res = await fetch( `/api/admin/billing/invoices/${invoice.id}/mark-paid`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ note: noteInput || undefined }), } ); const j = await res.json().catch(() => ({})); if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`); setNoteOpen(false); setNoteInput(""); router.refresh(); } catch (e: any) { setActionError(e.message); } finally { setBusyAction(null); } }; const deleteInvoice = async () => { if (!confirm(t("confirmDeleteInvoice", { num: invoice.invoiceNumber }))) return; setActionError(""); setBusyAction("delete"); try { const res = await fetch(`/api/admin/billing/invoices/${invoice.id}`, { method: "DELETE", }); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j.error || `HTTP ${res.status}`); } router.push("/admin/billing/invoices"); } catch (e: any) { setActionError(e.message); setBusyAction(null); } }; // Group lines by tenant for display (matches PDF layout). const linesByTenant = new Map(); for (const ln of lines) { const k = ln.tenantName; if (!linesByTenant.has(k)) linesByTenant.set(k, []); linesByTenant.get(k)!.push(ln); } const tenantOrder = [...linesByTenant.keys()].sort((a, b) => { if (a === null) return 1; if (b === null) return -1; return a.localeCompare(b); }); return (

{invoice.invoiceNumber}

{invoice.periodStart} → {invoice.periodEnd} · {t("dueOnLabel")}: {invoice.dueAt} · {invoice.locale}
{t("totalLabel")}
CHF {invoice.totalChf.toFixed(2)}
{/* Action bar */}
{invoice.hasPdf && ( {t("downloadPdfBtn")} )} {(invoice.status === "open" || invoice.status === "overdue") && ( <> {!noteOpen ? ( ) : (
setNoteInput(e.target.value)} className="flex-grow px-3 py-1.5 rounded-md border border-border bg-surface-2 text-sm" autoFocus />
)} )}
{actionError && (
{actionError}
)} {invoice.paidAt && (
{t("paidOnLabel")}: {invoice.paidAt} · {invoice.paidBy} ·{" "} {invoice.paidMethodDetail}
)}
{/* Lines */} {t("lineItemsTitle")} {tenantOrder.map((tenantKey) => { const tenantLines = linesByTenant.get(tenantKey)!; return ( {tenantKey && ( )} {tenantLines.map((ln) => ( ))} ); })}
{t("descCol")} {t("qtyCol")} {t("unitPriceCol")} {t("amountCol")}
{tenantKey}
{ln.description}
{ln.kind}
{ln.quantity} {ln.unitLabel ? ` ${ln.unitLabel}` : ""} {ln.unitPriceChf.toFixed(4)} {ln.amountChf.toFixed(2)}
{t("subtotal")} CHF {invoice.subtotalChf.toFixed(2)}
{t("vat")} ({invoice.vatRate.toFixed(2)}%) CHF {invoice.vatAmountChf.toFixed(2)}
{t("total")} CHF {invoice.totalChf.toFixed(2)}
{/* Billing snapshot */} {t("billToSnapshotTitle")}
{invoice.billingSnapshot.companyName}
{invoice.billingSnapshot.streetAddress}
{invoice.billingSnapshot.postalCode}{" "} {invoice.billingSnapshot.city}
{invoice.billingSnapshot.country}
{invoice.billingSnapshot.vatNumber && (
VAT: {invoice.billingSnapshot.vatNumber}
)}
{invoice.billingSnapshot.billingEmail}
); } function StatusPill({ status }: { status: InvoiceStatus }) { const t = useTranslations("adminBilling"); const color = status === "paid" ? "bg-success/15 text-success" : status === "overdue" ? "bg-error/15 text-error" : status === "void" || status === "uncollectible" ? "bg-text-muted/15 text-text-muted" : "bg-accent/15 text-accent"; return ( {t(`status_${status}`)} ); }