Files
pieced-portal/src/components/admin/billing/org-payment-mode-list.tsx
admin ee6bb89fb6
Some checks failed
Build and Push / build (push) Failing after 42s
Phase8: Auto bill credit card
2026-05-27 22:06:32 +02:00

159 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>
)}
<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>
);
}