import React from 'react'; import { Loader2, RefreshCw, Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { AdminLayout } from '../components/AdminLayout'; import { getTenantPackagesOverview, getTenantPaddleTransactions, PaddleTransactionSummary, TenantPackageSummary } from '../api'; import { isAuthError } from '../auth/tokens'; export default function BillingPage() { const { t, i18n } = useTranslation(['management', 'dashboard']); const locale = React.useMemo( () => (i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE'), [i18n.language] ); const [packages, setPackages] = React.useState([]); const [activePackage, setActivePackage] = React.useState(null); const [transactions, setTransactions] = React.useState([]); const [transactionCursor, setTransactionCursor] = React.useState(null); const [transactionsHasMore, setTransactionsHasMore] = React.useState(false); const [transactionsLoading, setTransactionsLoading] = React.useState(false); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const formatDate = React.useCallback( (value: string | null | undefined) => { if (!value) return '--'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '--'; return date.toLocaleDateString(locale, { day: '2-digit', month: 'short', year: 'numeric' }); }, [locale] ); const formatCurrency = React.useCallback( (value: number | null | undefined, currency = 'EUR') => { if (value === null || value === undefined) return '--'; return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value); }, [locale] ); const packageLabels = React.useMemo( () => ({ statusActive: t('billing.sections.packages.card.statusActive'), statusInactive: t('billing.sections.packages.card.statusInactive'), used: t('billing.sections.packages.card.used'), available: t('billing.sections.packages.card.available'), expires: t('billing.sections.packages.card.expires'), }), [t] ); const loadAll = React.useCallback(async () => { setLoading(true); setError(null); try { const [packagesResult, paddleTransactions] = await Promise.all([ getTenantPackagesOverview(), getTenantPaddleTransactions().catch((err) => { console.warn('Failed to load Paddle transactions', err); return { data: [] as PaddleTransactionSummary[], nextCursor: null, hasMore: false }; }), ]); setPackages(packagesResult.packages); setActivePackage(packagesResult.activePackage); setTransactions(paddleTransactions.data); setTransactionCursor(paddleTransactions.nextCursor); setTransactionsHasMore(paddleTransactions.hasMore); } catch (err) { if (!isAuthError(err)) { setError(t('billing.errors.load')); } } finally { setLoading(false); } }, [t]); const loadMoreTransactions = React.useCallback(async () => { if (!transactionsHasMore || transactionsLoading || !transactionCursor) { return; } setTransactionsLoading(true); try { const result = await getTenantPaddleTransactions(transactionCursor); setTransactions((current) => [...current, ...result.data]); setTransactionCursor(result.nextCursor); setTransactionsHasMore(result.hasMore && Boolean(result.nextCursor)); } catch (error) { console.warn('Failed to load additional Paddle transactions', error); setTransactionsHasMore(false); } finally { setTransactionsLoading(false); } }, [transactionCursor, transactionsHasMore, transactionsLoading]); React.useEffect(() => { void loadAll(); }, [loadAll]); const actions = ( ); return ( {error && ( {t('dashboard:alerts.errorTitle')} {error} )} {loading ? ( ) : ( <>
{t('billing.sections.overview.title')} {t('billing.sections.overview.description')}
{activePackage ? activePackage.package_name : t('billing.sections.overview.emptyBadge')}
{activePackage ? (
) : ( )}
{t('billing.sections.packages.title')} {t('billing.sections.packages.description')} {packages.length === 0 ? ( ) : ( packages.map((pkg) => ( )) )} {t('billing.sections.transactions.title')} {t('billing.sections.transactions.description')} {transactions.length === 0 ? ( ) : (
{transactions.map((transaction) => ( ))}
)} {transactionsHasMore && ( )}
)}
); } function TransactionCard({ transaction, formatCurrency, formatDate, locale, t, }: { transaction: PaddleTransactionSummary; formatCurrency: (value: number | null | undefined, currency?: string) => string; formatDate: (value: string | null | undefined) => string; locale: string; t: (key: string, options?: Record) => string; }) { const amount = transaction.grand_total ?? transaction.amount ?? null; const currency = transaction.currency ?? 'EUR'; const createdAtIso = transaction.created_at ?? null; const createdAt = createdAtIso ? new Date(createdAtIso) : null; const createdLabel = createdAt ? createdAt.toLocaleString(locale, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', }) : formatDate(createdAtIso); const statusKey = transaction.status ? `billing.sections.transactions.status.${transaction.status}` : 'billing.sections.transactions.status.unknown'; const statusText = t(statusKey, { defaultValue: (transaction.status ?? 'unknown').replace(/_/g, ' '), }); return (

{t('billing.sections.transactions.labels.transactionId', { id: transaction.id ?? '—' })}

{createdLabel}

{transaction.checkout_id && (

{t('billing.sections.transactions.labels.checkoutId', { id: transaction.checkout_id })}

)} {transaction.origin && (

{t('billing.sections.transactions.labels.origin', { origin: transaction.origin })}

)}
{statusText}
{formatCurrency(amount, currency)}
{transaction.tax !== undefined && transaction.tax !== null && ( {t('billing.sections.transactions.labels.tax', { value: formatCurrency(transaction.tax, currency) })} )} {transaction.receipt_url && ( {t('billing.sections.transactions.labels.receipt')} )}
); } function InfoCard({ label, value, helper, tone, }: { label: string; value: string | number | null | undefined; helper?: string; tone: 'pink' | 'amber' | 'sky' | 'emerald'; }) { const toneClass = { pink: 'from-pink-50 to-rose-100 text-pink-700', amber: 'from-amber-50 to-yellow-100 text-amber-700', sky: 'from-sky-50 to-blue-100 text-sky-700', emerald: 'from-emerald-50 to-green-100 text-emerald-700', }[tone]; return (
{label}
{value ?? '--'}
{helper &&

{helper}

}
); } function PackageCard({ pkg, isActive, labels, formatDate, formatCurrency, }: { pkg: TenantPackageSummary; isActive: boolean; labels: { statusActive: string; statusInactive: string; used: string; available: string; expires: string; }; formatDate: (value: string | null | undefined) => string; formatCurrency: (value: number | null | undefined, currency?: string) => string; }) { return (

{pkg.package_name}

{formatDate(pkg.purchased_at)} · {formatCurrency(pkg.price, pkg.currency ?? 'EUR')}

{isActive ? labels.statusActive : labels.statusInactive}
{labels.used}: {pkg.used_events} {labels.available}: {pkg.remaining_events ?? '--'} {labels.expires}: {formatDate(pkg.expires_at)}
); } function EmptyState({ message }: { message: string }) { return (

{message}

); } function BillingSkeleton() { return (
{Array.from({ length: 3 }).map((_, index) => (
{Array.from({ length: 4 }).map((__, placeholderIndex) => (
))}
))}
); }