'use client' import type { ReactNode } from 'react' import { useEffect, useMemo, useState, useTransition } from 'react' import { useAdminI18n } from '@/components/I18nProvider' import { ADMIN_API_BASE } from '@/lib/api' interface BillingStats { billingAccountCount: number openInvoiceCount: number pastDueInvoiceCount: number paidInvoiceCount: number accountsReceivableTotal: number recognizedRevenueTotal: number } interface SubscriptionSummary { id: string plan: string billingPeriod: string status: string currentPeriodEnd: string | null cancelAtPeriodEnd: boolean } interface CompanySummary { id: string name: string email: string slug: string status: string subscription: SubscriptionSummary | null } interface CreditBalance { id: string currency: string balanceAmount: number } interface InvoiceLineItem { id?: string type?: string description: string quantity: number unitAmount: number amount: number currency: string periodStart?: string | null periodEnd?: string | null } interface PaymentAttempt { id: string status: string amount: number currency: string providerPaymentId?: string | null failureCode?: string | null failureMessage?: string | null attemptedAt: string } interface CreditNote { id: string amount: number currency: string reason: string status: string createdAt: string } interface Refund { id: string amount: number currency: string reason: string status: string createdAt: string } interface TaxRecord { id: string taxAmount: number taxRate?: number | null taxExempt: boolean taxType?: string | null } interface BillingEvent { id: string eventType: string source: string createdAt: string } interface BillingInvoice { id: string invoiceNumber: string | null invoiceType: string status: string currency: string totalAmount: number amountPaid: number amountDue: number invoiceDate: string | null dueAt: string | null paidAt: string | null createdAt: string isSubscriptionBlocking: boolean lineItems: InvoiceLineItem[] paymentAttempts: PaymentAttempt[] creditNotes: CreditNote[] refunds: Refund[] taxRecords: TaxRecord[] } interface BillingAccountSummary { id: string companyId: string legalName: string billingEmail: string invoiceTerms: string netTermsDays: number taxId?: string | null taxExempt: boolean dunningPaused: boolean createdAt: string company: CompanySummary invoices: BillingInvoice[] creditBalances: CreditBalance[] openBalance: number paidBalance: number creditBalance: number } interface BillingAccountDetail extends BillingAccountSummary { billingAddress?: Record | null paymentMethods: Array<{ id: string type: string label?: string | null brand?: string | null last4?: string | null isDefault: boolean isActive: boolean }> creditLedgerEntries: Array<{ id: string type: string amount: number currency: string reason: string createdAt: string }> events: BillingEvent[] } interface PaginatedResponse { data: T[] total: number page: number pageSize: number totalPages: number stats?: BillingStats } interface DraftLineItem { type: string description: string quantity: number unitAmount: number } const ACCOUNT_STATUS_COLORS: Record = { ACTIVE: 'text-emerald-300 bg-emerald-500/15', TRIALING: 'text-sky-300 bg-sky-500/15', PAYMENT_PENDING: 'text-amber-300 bg-amber-500/15', PAST_DUE: 'text-rose-300 bg-rose-500/15', SUSPENDED: 'text-red-300 bg-red-500/15', CANCELLED: 'text-zinc-300 bg-zinc-700/60', UNPAID: 'text-red-300 bg-red-500/15', } const INVOICE_STATUS_COLORS: Record = { DRAFT: 'text-zinc-300 bg-zinc-700/60', OPEN: 'text-amber-300 bg-amber-500/15', PAYMENT_PENDING: 'text-amber-300 bg-amber-500/15', PARTIALLY_PAID: 'text-sky-300 bg-sky-500/15', PAID: 'text-emerald-300 bg-emerald-500/15', PAST_DUE: 'text-rose-300 bg-rose-500/15', VOID: 'text-zinc-300 bg-zinc-700/60', UNCOLLECTIBLE: 'text-red-200 bg-red-950/60', REFUNDED: 'text-zinc-300 bg-zinc-700/60', PARTIALLY_REFUNDED: 'text-zinc-300 bg-zinc-700/60', } const FIELD_CLASS = 'w-full rounded-xl border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm text-zinc-100 outline-none transition focus:border-blue-700' const ACTION_PRIMARY_CLASS = 'rounded-xl bg-emerald-500 px-4 py-2 text-sm font-semibold text-black hover:bg-emerald-400' const ACTION_SECONDARY_CLASS = 'rounded-xl border border-zinc-700 px-4 py-2 text-sm font-semibold text-zinc-200 hover:border-zinc-500' const draftLineItem = (): DraftLineItem => ({ type: 'PROFESSIONAL_SERVICES', description: '', quantity: 1, unitAmount: 0, }) function money(amount: number, currency: string) { return new Intl.NumberFormat('en-GB', { style: 'currency', currency, minimumFractionDigits: 2, }).format(amount / 100) } function dateLabel(value: string | null | undefined) { if (!value) return '—' return new Date(value).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) } function termsLabel(invoiceTerms: string, netTermsDays: number) { if (invoiceTerms === 'DUE_ON_RECEIPT') return 'Due on receipt' return `Net ${netTermsDays || invoiceTerms.replace('NET_', '')}` } export default function AdminBillingPage() { const { language } = useAdminI18n() const [isPending, startTransition] = useTransition() const copy = { en: { title: 'Billing Console', eyebrow: 'Platform Finance', searchPlaceholder: 'Search company, billing contact, or slug', all: 'All', active: 'Active', pastDue: 'Past Due', suspended: 'Suspended', cancelled: 'Cancelled', accounts: 'Accounts', receivables: 'Receivables', openInvoices: 'Open invoices', paidInvoices: 'Paid invoices', revenue: 'Revenue', billingAccounts: 'Billing Accounts', selectAccount: 'Select a billing account to manage invoices, credits, dunning, and payment history.', company: 'Company', plan: 'Plan', subscription: 'Subscription', openBalance: 'Open balance', credit: 'Credit', dunning: 'Dunning', accountDetails: 'Billing Account', invoiceFeed: 'Invoices', draftInvoice: 'Create Draft Invoice', saveAccount: 'Save account', pauseDunning: 'Pause dunning', resumeDunning: 'Resume dunning', createDraft: 'Create draft', loading: 'Loading…', noAccounts: 'No billing accounts found', amountDue: 'Amount due', amountPaid: 'Amount paid', dueDate: 'Due date', issueDate: 'Issue date', actions: 'Actions', lineItems: 'Line items', payments: 'Payment attempts', credits: 'Credits', refunds: 'Refunds', timeline: 'Timeline', finalize: 'Finalize', pay: 'Record payment', retry: 'Retry payment', void: 'Void', writeoff: 'Write off', refund: 'Refund', creditNote: 'Credit note', downloadPdf: 'PDF', invoiceType: 'Invoice type', blocking: 'Blocking invoice', reason: 'Reason', billingEmail: 'Billing email', legalName: 'Legal name', taxId: 'Tax ID', taxExempt: 'Tax exempt', invoiceTerms: 'Invoice terms', selectedInvoice: 'Selected invoice', noInvoices: 'No invoices yet', }, fr: { title: 'Console de facturation', eyebrow: 'Finance plateforme', searchPlaceholder: 'Rechercher une entreprise, un contact facturation ou un slug', all: 'Tous', active: 'Actifs', pastDue: 'En retard', suspended: 'Suspendus', cancelled: 'Annulés', accounts: 'Comptes', receivables: 'Créances', openInvoices: 'Factures ouvertes', paidInvoices: 'Factures payées', revenue: 'Revenu', billingAccounts: 'Comptes de facturation', selectAccount: 'Sélectionnez un compte de facturation pour gérer factures, crédits, relances et paiements.', company: 'Entreprise', plan: 'Plan', subscription: 'Abonnement', openBalance: 'Solde ouvert', credit: 'Crédit', dunning: 'Relance', accountDetails: 'Compte de facturation', invoiceFeed: 'Factures', draftInvoice: 'Créer un brouillon', saveAccount: 'Enregistrer', pauseDunning: 'Mettre en pause', resumeDunning: 'Reprendre', createDraft: 'Créer le brouillon', loading: 'Chargement…', noAccounts: 'Aucun compte trouvé', amountDue: 'Montant dû', amountPaid: 'Montant payé', dueDate: 'Échéance', issueDate: 'Date émission', actions: 'Actions', lineItems: 'Lignes', payments: 'Tentatives de paiement', credits: 'Crédits', refunds: 'Remboursements', timeline: 'Historique', finalize: 'Finaliser', pay: 'Enregistrer paiement', retry: 'Relancer paiement', void: 'Annuler', writeoff: 'Passer en perte', refund: 'Rembourser', creditNote: 'Avoir', downloadPdf: 'PDF', invoiceType: 'Type de facture', blocking: 'Facture bloquante', reason: 'Raison', billingEmail: 'Email de facturation', legalName: 'Raison sociale', taxId: 'Identifiant fiscal', taxExempt: 'Exonéré de taxe', invoiceTerms: 'Conditions', selectedInvoice: 'Facture sélectionnée', noInvoices: 'Aucune facture', }, ar: { title: 'لوحة الفوترة', eyebrow: 'مالية المنصة', searchPlaceholder: 'ابحث عن الشركة أو جهة الفوترة أو الرابط', all: 'الكل', active: 'نشط', pastDue: 'متأخر', suspended: 'معلق', cancelled: 'ملغى', accounts: 'الحسابات', receivables: 'الذمم', openInvoices: 'الفواتير المفتوحة', paidInvoices: 'الفواتير المدفوعة', revenue: 'الإيراد', billingAccounts: 'حسابات الفوترة', selectAccount: 'اختر حساب فوترة لإدارة الفواتير والأرصدة ومحاولات الدفع.', company: 'الشركة', plan: 'الخطة', subscription: 'الاشتراك', openBalance: 'الرصيد المفتوح', credit: 'الرصيد', dunning: 'المتابعة', accountDetails: 'حساب الفوترة', invoiceFeed: 'الفواتير', draftInvoice: 'إنشاء مسودة فاتورة', saveAccount: 'حفظ', pauseDunning: 'إيقاف المتابعة', resumeDunning: 'استئناف المتابعة', createDraft: 'إنشاء المسودة', loading: 'جارٍ التحميل…', noAccounts: 'لا توجد حسابات فوترة', amountDue: 'المبلغ المستحق', amountPaid: 'المبلغ المدفوع', dueDate: 'تاريخ الاستحقاق', issueDate: 'تاريخ الإصدار', actions: 'الإجراءات', lineItems: 'البنود', payments: 'محاولات الدفع', credits: 'الأرصدة', refunds: 'الاستردادات', timeline: 'السجل', finalize: 'اعتماد', pay: 'تسجيل الدفع', retry: 'إعادة المحاولة', void: 'إلغاء', writeoff: 'شطب', refund: 'استرداد', creditNote: 'إشعار دائن', downloadPdf: 'PDF', invoiceType: 'نوع الفاتورة', blocking: 'فاتورة حاجزة', reason: 'السبب', billingEmail: 'بريد الفوترة', legalName: 'الاسم القانوني', taxId: 'الرقم الضريبي', taxExempt: 'معفى من الضريبة', invoiceTerms: 'شروط الفاتورة', selectedInvoice: 'الفاتورة المحددة', noInvoices: 'لا توجد فواتير', }, }[language] const [accounts, setAccounts] = useState([]) const [stats, setStats] = useState(null) const [search, setSearch] = useState('') const [statusFilter, setStatusFilter] = useState('') const [loadingList, setLoadingList] = useState(true) const [listError, setListError] = useState(null) const [selectedCompanyId, setSelectedCompanyId] = useState(null) const [detail, setDetail] = useState(null) const [loadingDetail, setLoadingDetail] = useState(false) const [detailError, setDetailError] = useState(null) const [accountForm, setAccountForm] = useState({ legalName: '', billingEmail: '', taxId: '', taxExempt: false, invoiceTerms: 'DUE_ON_RECEIPT', netTermsDays: 0, }) const [invoiceDraft, setInvoiceDraft] = useState({ invoiceType: 'MANUAL', isSubscriptionBlocking: false, adminReason: '', lineItems: [draftLineItem()], }) const [selectedInvoiceId, setSelectedInvoiceId] = useState(null) const [payAmount, setPayAmount] = useState('') const [creditAmount, setCreditAmount] = useState('') const [creditReason, setCreditReason] = useState('') const [refundAmount, setRefundAmount] = useState('') const [refundReason, setRefundReason] = useState('') const [voidReason, setVoidReason] = useState('') const [writeoffReason, setWriteoffReason] = useState('') const [actionError, setActionError] = useState(null) const [actionMessage, setActionMessage] = useState(null) const filters = [ { key: '', label: copy.all }, { key: 'ACTIVE', label: copy.active }, { key: 'PAST_DUE', label: copy.pastDue }, { key: 'SUSPENDED', label: copy.suspended }, { key: 'CANCELLED', label: copy.cancelled }, ] const selectedInvoice = useMemo( () => detail?.invoices.find((invoice) => invoice.id === selectedInvoiceId) ?? detail?.invoices[0] ?? null, [detail, selectedInvoiceId], ) async function api(path: string, init?: RequestInit): Promise { const res = await fetch(`${ADMIN_API_BASE}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}), }, credentials: 'include', cache: 'no-store', }) if (res.headers.get('content-type')?.includes('application/pdf')) { return await (res.blob() as unknown as Promise) } const json = await res.json() if (!res.ok) throw new Error(json?.message ?? `Request failed (${res.status})`) return json.data as T } async function fetchAccounts(q?: string, status?: string) { setLoadingList(true) setListError(null) try { const params = new URLSearchParams({ pageSize: '100' }) if (q) params.set('q', q) if (status) params.set('status', status) const data = await api>(`/admin/billing?${params.toString()}`) setAccounts(data.data) setStats(data.stats ?? null) setSelectedCompanyId((current) => current ?? data.data[0]?.company.id ?? null) } catch (error: any) { setListError(error.message) } finally { setLoadingList(false) } } async function fetchDetail(companyId: string) { setLoadingDetail(true) setDetailError(null) try { const data = await api(`/admin/billing/${companyId}`) setDetail(data) setAccountForm({ legalName: data.legalName, billingEmail: data.billingEmail, taxId: data.taxId ?? '', taxExempt: data.taxExempt, invoiceTerms: data.invoiceTerms, netTermsDays: data.netTermsDays, }) setSelectedInvoiceId(data.invoices[0]?.id ?? null) } catch (error: any) { setDetailError(error.message) } finally { setLoadingDetail(false) } } async function refreshSelectedAccount() { await fetchAccounts(search || undefined, statusFilter || undefined) if (selectedCompanyId) await fetchDetail(selectedCompanyId) } useEffect(() => { const id = setTimeout(() => { fetchAccounts(search || undefined, statusFilter || undefined) }, 200) return () => clearTimeout(id) }, [search, statusFilter]) useEffect(() => { if (!selectedCompanyId) return fetchDetail(selectedCompanyId) }, [selectedCompanyId]) function setDraftLine(index: number, key: keyof DraftLineItem, value: string | number) { setInvoiceDraft((current) => ({ ...current, lineItems: current.lineItems.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: value } : item, ), })) } async function saveBillingAccount() { if (!detail) return setActionError(null) setActionMessage(null) try { await api(`/admin/billing/accounts/${detail.id}`, { method: 'PATCH', body: JSON.stringify({ legalName: accountForm.legalName, billingEmail: accountForm.billingEmail, taxId: accountForm.taxId || null, taxExempt: accountForm.taxExempt, invoiceTerms: accountForm.invoiceTerms, netTermsDays: accountForm.netTermsDays, }), }) setActionMessage('Billing account updated.') await refreshSelectedAccount() } catch (error: any) { setActionError(error.message) } } async function toggleDunning(paused: boolean) { if (!detail) return setActionError(null) setActionMessage(null) try { await api(`/admin/billing/accounts/${detail.id}/${paused ? 'pause-dunning' : 'resume-dunning'}`, { method: 'POST', body: JSON.stringify({}), }) setActionMessage(paused ? 'Dunning paused.' : 'Dunning resumed.') await refreshSelectedAccount() } catch (error: any) { setActionError(error.message) } } async function createDraftInvoice() { if (!detail) return setActionError(null) setActionMessage(null) try { await api(`/admin/billing/accounts/${detail.id}/invoices`, { method: 'POST', body: JSON.stringify({ invoiceType: invoiceDraft.invoiceType, isSubscriptionBlocking: invoiceDraft.isSubscriptionBlocking, adminReason: invoiceDraft.adminReason || null, lineItems: invoiceDraft.lineItems.map((item) => ({ ...item, unitAmount: Number(item.unitAmount), quantity: Number(item.quantity), })), }), }) setInvoiceDraft({ invoiceType: 'MANUAL', isSubscriptionBlocking: false, adminReason: '', lineItems: [draftLineItem()], }) setActionMessage('Draft invoice created.') await refreshSelectedAccount() } catch (error: any) { setActionError(error.message) } } async function runInvoiceAction(path: string, body?: Record) { if (!selectedInvoice) return setActionError(null) setActionMessage(null) try { await api(`/admin/billing/invoices/${selectedInvoice.id}/${path}`, { method: 'POST', body: JSON.stringify(body ?? {}), }) setActionMessage('Invoice action completed.') await refreshSelectedAccount() } catch (error: any) { setActionError(error.message) } } async function downloadPdf() { if (!selectedInvoice) return setActionError(null) try { const blob = await api(`/admin/billing/invoices/${selectedInvoice.id}/pdf`) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `${selectedInvoice.invoiceNumber ?? selectedInvoice.id}.pdf` a.click() URL.revokeObjectURL(url) } catch (error: any) { setActionError(error.message) } } return (

{copy.eyebrow}

{copy.title}

setSearch(event.target.value)} placeholder={copy.searchPlaceholder} className="w-full rounded-2xl border border-zinc-800 bg-zinc-950 px-4 py-3 text-sm text-zinc-100 outline-none transition focus:border-blue-700" />
{stats && (
)}
{filters.map((filter) => ( ))}

{copy.billingAccounts}

{loadingList ? (
{copy.loading}
) : listError ? (
{listError}
) : accounts.length === 0 ? (
{copy.noAccounts}
) : (
{accounts.map((account) => { const isSelected = selectedCompanyId === account.company.id const subscription = account.company.subscription return ( ) })}
)}
{!selectedCompanyId ? (
{copy.selectAccount}
) : loadingDetail && !detail ? (
{copy.loading}
) : detailError ? (
{detailError}
) : detail ? ( <>

{copy.accountDetails}

{detail.company.name}

{detail.company.subscription?.status ?? 'NO_SUBSCRIPTION'} {detail.company.subscription?.plan ?? 'No plan'} / {detail.company.subscription?.billingPeriod ?? '—'} {termsLabel(detail.invoiceTerms, detail.netTermsDays)}
setAccountForm((current) => ({ ...current, legalName: event.target.value }))} className={FIELD_CLASS} /> setAccountForm((current) => ({ ...current, billingEmail: event.target.value }))} className={FIELD_CLASS} /> setAccountForm((current) => ({ ...current, taxId: event.target.value }))} className={FIELD_CLASS} />

{copy.invoiceFeed}

{isPending || loadingDetail ? {copy.loading} : null}
{detail.invoices.length === 0 ? (
{copy.noInvoices}
) : (
{detail.invoices.map((invoice) => ( setSelectedInvoiceId(invoice.id)} > ))}
Invoice {copy.amountDue} {copy.dueDate} {copy.actions}

{invoice.invoiceNumber ?? invoice.invoiceType}

{dateLabel(invoice.invoiceDate ?? invoice.createdAt)}

{money(invoice.amountDue, invoice.currency)} {dateLabel(invoice.dueAt)} {invoice.status}
{selectedInvoice && (

{copy.selectedInvoice}

{selectedInvoice.invoiceNumber ?? selectedInvoice.invoiceType}

{copy.issueDate}: {dateLabel(selectedInvoice.invoiceDate ?? selectedInvoice.createdAt)}

{selectedInvoice.status}

{copy.lineItems}

{selectedInvoice.lineItems.map((item, index) => (

{item.description}

{item.quantity} × {money(item.unitAmount, item.currency)}

{money(item.amount, item.currency)}

))}
{selectedInvoice.status === 'DRAFT' ? ( ) : null} {['OPEN', 'PAYMENT_PENDING', 'PAST_DUE', 'PARTIALLY_PAID'].includes(selectedInvoice.status) ? ( <>
setPayAmount(event.target.value)} placeholder="Amount in cents" className={FIELD_CLASS} />
setVoidReason(event.target.value)} placeholder={copy.reason} className={FIELD_CLASS} />
setWriteoffReason(event.target.value)} placeholder={copy.reason} className={FIELD_CLASS} />
setCreditAmount(event.target.value)} placeholder="Cents" className={FIELD_CLASS} /> setCreditReason(event.target.value)} placeholder={copy.reason} className={FIELD_CLASS} />
) : null} {['PAID', 'PARTIALLY_REFUNDED', 'REFUNDED'].includes(selectedInvoice.status) ? (
setRefundAmount(event.target.value)} placeholder="Cents" className={FIELD_CLASS} /> setRefundReason(event.target.value)} placeholder={copy.reason} className={FIELD_CLASS} />
) : null} {selectedInvoice.invoiceNumber ? ( ) : null}
({ id: attempt.id, primary: `${attempt.status} · ${money(attempt.amount, attempt.currency)}`, secondary: `${dateLabel(attempt.attemptedAt)}${attempt.failureMessage ? ` · ${attempt.failureMessage}` : ''}`, }))} /> ({ id: note.id, primary: `${money(note.amount, note.currency)} · ${note.status}`, secondary: note.reason, }))} /> ({ id: refund.id, primary: `${money(refund.amount, refund.currency)} · ${refund.status}`, secondary: refund.reason, }))} />
)}
)}

{copy.draftInvoice}

setInvoiceDraft((current) => ({ ...current, adminReason: event.target.value }))} className={FIELD_CLASS} />
{invoiceDraft.lineItems.map((item, index) => (
setDraftLine(index, 'description', event.target.value)} placeholder="Description" className={FIELD_CLASS} /> setDraftLine(index, 'quantity', Number(event.target.value))} className={FIELD_CLASS} /> setDraftLine(index, 'unitAmount', Number(event.target.value))} placeholder="Unit amount (cents)" className={FIELD_CLASS} />
))}
({ id: event.id, primary: event.eventType, secondary: `${event.source} · ${dateLabel(event.createdAt)}`, }))} /> ({ id: entry.id, primary: `${money(entry.amount, entry.currency)} · ${entry.type}`, secondary: entry.reason, }))} />
) : null}
{actionError ?
{actionError}
: null} {actionMessage ?
{actionMessage}
: null}
) } function StatCard({ label, value, tone }: { label: string; value: string; tone: string }) { return (

{label}

{value}

) } function Metric({ label, value }: { label: string; value: string }) { return (

{label}

{value}

) } function Field({ label, children }: { label: string; children: ReactNode }) { return ( ) } function DetailList({ title, items, }: { title: string items: Array<{ id: string; primary: string; secondary: string }> }) { return (

{title}

{items.length === 0 ? (
) : (
{items.map((item) => (

{item.primary}

{item.secondary}

))}
)}
) }