import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { getSessionUser } from "@/lib/session"; import { listTenants } from "@/lib/k8s"; import { getOrgBilling } from "@/lib/db"; import { BackLink } from "@/components/ui/back-link"; import { GenerateForm } from "@/components/admin/billing/generate-form"; /** * /admin/billing/generate — testing tool to compute & commit an * invoice for a given (org, period). * * Workflow: * 1. Admin picks org + year/month + locale (default auto-detected * from country). * 2. "Preview" runs computeInvoiceDraft (dryRun) — shows lines, * totals, warnings. * 3. "Commit" persists + renders the PDF. * * The org dropdown is hydrated server-side here so the page loads * with the list pre-populated. Per-org billing status (address * present / open balance) is fetched on demand from /api/admin/ * billing/orgs since it can change as admin edits. */ export default async function AdminBillingGeneratePage() { const user = await getSessionUser(); if (!user) redirect("/login"); if (!user.isPlatform) redirect("/dashboard"); const t = await getTranslations("adminBilling"); // Build initial org list from tenant labels. const tenants = await listTenants(); const orgMap = new Map(); for (const t of tenants) { const oid = t.metadata.labels?.["pieced.ch/zitadel-org-id"]; if (!oid) continue; if (!orgMap.has(oid)) orgMap.set(oid, []); orgMap.get(oid)!.push(t.metadata.name); } // Hydrate company name + country in parallel. const orgList = await Promise.all( [...orgMap.entries()].map(async ([orgId, tenantNames]) => { const billing = await getOrgBilling(orgId).catch(() => null); return { zitadelOrgId: orgId, tenantNames, companyName: billing?.companyName ?? null, country: billing?.country ?? null, hasBillingAddress: !!billing, }; }) ); orgList.sort((a, b) => (a.companyName ?? a.zitadelOrgId).localeCompare( b.companyName ?? b.zitadelOrgId ) ); return (

{t("generateTitle")}

{t("generatePageDesc")}

); }