"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 { InvoiceDraft } from "@/types"; interface OrgEntry { zitadelOrgId: string; tenantNames: string[]; companyName: string | null; country: string | null; hasBillingAddress: boolean; } interface Props { orgs: OrgEntry[]; } const LOCALE_OPTIONS = [ { value: "de", label: "Deutsch" }, { value: "en", label: "English" }, { value: "fr", label: "Français" }, { value: "it", label: "Italiano" }, ]; /** * Two-step flow: preview (dryRun) → commit. * * Preview displays the InvoiceDraft (lines, subtotal, VAT, total) * plus any warnings. Admin reviews and either commits or aborts. * Commit re-runs the generator without dryRun and redirects to the * persisted invoice's detail page. */ export function GenerateForm({ orgs }: Props) { const t = useTranslations("adminBilling"); const router = useRouter(); // Default to previous calendar month — that's the typical "bill // for last month" use case. const now = new Date(); const prevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); const [orgId, setOrgId] = useState(orgs[0]?.zitadelOrgId ?? ""); const [year, setYear] = useState(String(prevMonth.getFullYear())); const [month, setMonth] = useState(String(prevMonth.getMonth() + 1)); const [locale, setLocale] = useState(""); const [draft, setDraft] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const selectedOrg = orgs.find((o) => o.zitadelOrgId === orgId); // Auto-detect default locale from country if admin hasn't picked // one. Same logic as billing.ts's defaultLocaleForCountry. const effectiveLocale = locale || (() => { const c = (selectedOrg?.country || "").toUpperCase(); if (["CH", "LI", "AT", "DE"].includes(c)) return "de"; if (["FR", "BE", "LU"].includes(c)) return "fr"; if (c === "IT") return "it"; return "en"; })(); const preview = async () => { setError(""); setDraft(null); setBusy(true); try { const res = await fetch("/api/admin/billing/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ zitadelOrgId: orgId, year: Number(year), month: Number(month), locale: effectiveLocale, dryRun: true, }), }); const j = await res.json(); if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`); setDraft(j.draft); } catch (e: any) { setError(e.message); } finally { setBusy(false); } }; const commit = async () => { if (!draft) return; if (!confirm(t("confirmGenerate"))) return; setError(""); setBusy(true); try { const res = await fetch("/api/admin/billing/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ zitadelOrgId: orgId, year: Number(year), month: Number(month), locale: effectiveLocale, dryRun: false, }), }); const j = await res.json(); if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`); // Navigate to the new invoice's detail page. if (j.invoice?.id) { router.push(`/admin/billing/invoices/${j.invoice.id}`); } } catch (e: any) { setError(e.message); setBusy(false); } }; return (
{t("generateFormTitle")} {orgs.length === 0 ? (

{t("noOrgsToGenerate")}

) : (
{draft && ( )} {error && ( {error} )}
)}
{draft && }
); } function DraftPreview({ draft }: { draft: InvoiceDraft }) { const t = useTranslations("adminBilling"); // Group lines by tenant for the preview (matches PDF layout). const linesByTenant = new Map(); for (const ln of draft.lines) { const key = ln.tenantName; if (!linesByTenant.has(key)) linesByTenant.set(key, []); linesByTenant.get(key)!.push(ln); } const tenantOrder = [...linesByTenant.keys()].sort((a, b) => { if (a === null) return 1; if (b === null) return -1; return a.localeCompare(b); }); return ( {t("previewTitle")} — {draft.periodStart} → {draft.periodEnd} {draft.warnings.length > 0 && (
{t("warningsTitle")}
{draft.warnings.map((w, i) => (
• {w}
))}
)} {tenantOrder.map((tenantKey) => { const lines = linesByTenant.get(tenantKey)!; return ( {tenantKey && ( )} {lines.map((ln, i) => ( ))} ); })} {draft.lines.length === 0 && ( )}
{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("noLinesGenerated")}
{t("subtotal")} CHF {draft.subtotalChf.toFixed(2)}
{t("vat")} ({draft.vatRate.toFixed(2)}%) CHF {draft.vatAmountChf.toFixed(2)}
{t("total")} CHF {draft.totalChf.toFixed(2)}
); }