72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
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<string, string[]>();
|
|
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 (
|
|
<main className="max-w-4xl mx-auto px-6 py-8">
|
|
<BackLink href="/admin/billing" label={t("backToBilling")} />
|
|
<div className="mb-8 animate-in">
|
|
<h1 className="font-display text-2xl font-semibold accent-rule">
|
|
{t("generateTitle")}
|
|
</h1>
|
|
<p className="text-sm text-text-secondary mt-3">{t("generatePageDesc")}</p>
|
|
</div>
|
|
<GenerateForm orgs={orgList} />
|
|
</main>
|
|
);
|
|
}
|