161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useTranslations } from "next-intl";
|
|
import { Card } from "@/components/ui/card";
|
|
|
|
interface OrgEntry {
|
|
zitadelOrgId: string;
|
|
companyName: string | null;
|
|
country: string | null;
|
|
hasSavedCard: boolean;
|
|
cardLabel: string | null;
|
|
payByInvoice: boolean;
|
|
autoChargeEnabled: boolean;
|
|
}
|
|
|
|
interface Props {
|
|
orgs: OrgEntry[];
|
|
}
|
|
|
|
/**
|
|
* Inline toggles for pay_by_invoice and auto_charge_enabled per
|
|
* org. Each toggle round-trips to /api/admin/billing/orgs/[orgId]
|
|
* /payment-mode and then router.refresh() so the server-fetched
|
|
* state stays canonical (avoids drift between optimistic UI and
|
|
* the DB).
|
|
*
|
|
* Phase 9b-2.
|
|
*/
|
|
export function OrgPaymentModeList({ orgs }: Props) {
|
|
const t = useTranslations("adminBilling");
|
|
const router = useRouter();
|
|
const [busy, setBusy] = useState<string | null>(null);
|
|
const [error, setError] = useState("");
|
|
|
|
const toggle = async (
|
|
orgId: string,
|
|
patch: { payByInvoice?: boolean; autoChargeEnabled?: boolean }
|
|
) => {
|
|
setError("");
|
|
setBusy(orgId);
|
|
try {
|
|
const res = await fetch(
|
|
`/api/admin/billing/orgs/${encodeURIComponent(orgId)}/payment-mode`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(patch),
|
|
}
|
|
);
|
|
const j = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`);
|
|
router.refresh();
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
};
|
|
|
|
if (orgs.length === 0) {
|
|
return (
|
|
<Card>
|
|
<div className="p-6 text-center text-text-secondary text-sm">
|
|
{t("orgsEmpty")}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
{error && (
|
|
<div className="text-sm text-error border-b border-error/30 bg-error/10 px-4 py-2">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="text-xs text-text-muted text-left">
|
|
<tr>
|
|
<th className="pb-2 pl-3 pr-4">{t("orgsColCustomer")}</th>
|
|
<th className="pb-2 pr-4">{t("orgsColCard")}</th>
|
|
<th className="pb-2 pr-4 text-center">
|
|
{t("orgsColPayByInvoice")}
|
|
</th>
|
|
<th className="pb-2 pr-4 text-center">
|
|
{t("orgsColAutoCharge")}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{orgs.map((o) => (
|
|
<tr key={o.zitadelOrgId} className="border-t border-border">
|
|
<td className="py-2 pl-3 pr-4">
|
|
<div className="font-medium">
|
|
{o.companyName ?? (
|
|
<span className="font-mono text-xs">{o.zitadelOrgId}</span>
|
|
)}
|
|
</div>
|
|
{o.country && (
|
|
<div className="text-xs text-text-muted">{o.country}</div>
|
|
)}
|
|
</td>
|
|
<td className="py-2 pr-4 text-xs">
|
|
{o.hasSavedCard ? (
|
|
<span className="font-mono">{o.cardLabel}</span>
|
|
) : (
|
|
<span className="text-text-muted">
|
|
{t("orgsNoSavedCard")}
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td className="py-2 pr-4 text-center">
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={o.payByInvoice}
|
|
disabled={busy === o.zitadelOrgId}
|
|
onChange={(e) =>
|
|
toggle(o.zitadelOrgId, {
|
|
payByInvoice: e.target.checked,
|
|
})
|
|
}
|
|
/>
|
|
<span className="text-xs">
|
|
{o.payByInvoice
|
|
? t("orgsPayByInvoiceOn")
|
|
: t("orgsPayByInvoiceOff")}
|
|
</span>
|
|
</label>
|
|
</td>
|
|
<td className="py-2 pr-4 text-center">
|
|
<label className="inline-flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={o.autoChargeEnabled}
|
|
disabled={busy === o.zitadelOrgId || o.payByInvoice}
|
|
onChange={(e) =>
|
|
toggle(o.zitadelOrgId, {
|
|
autoChargeEnabled: e.target.checked,
|
|
})
|
|
}
|
|
/>
|
|
<span className="text-xs">
|
|
{o.autoChargeEnabled
|
|
? t("orgsAutoChargeOn")
|
|
: t("orgsAutoChargeOff")}
|
|
</span>
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|