This commit is contained in:
158
src/components/admin/billing/org-payment-mode-list.tsx
Normal file
158
src/components/admin/billing/org-payment-mode-list.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
"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>
|
||||
)}
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user