// @ts-nocheck import React from 'react'; import { AlertTriangle, Loader2, RefreshCw, Sparkles, ArrowUpRight } 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 { Separator } from '@/components/ui/separator'; import { AdminLayout } from '../components/AdminLayout'; import { getTenantPackagesOverview, getTenantPaddleTransactions, getTenantAddonHistory, PaddleTransactionSummary, TenantAddonHistoryEntry, TenantPackageSummary, PaginationMeta, } from '../api'; import { isAuthError } from '../auth/tokens'; import { FrostedSurface, SectionCard, SectionHeader } from '../components/tenant'; type PackageWarning = { id: string; tone: 'warning' | 'danger'; message: string }; 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 [addonHistory, setAddonHistory] = React.useState([]); const [addonMeta, setAddonMeta] = React.useState(null); const [addonsLoading, setAddonsLoading] = 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 resolveEventName = React.useCallback( (event: TenantAddonHistoryEntry['event']) => { const fallback = t('billing.sections.addOns.table.eventFallback', 'Event removed'); if (!event) { return fallback; } if (typeof event.name === 'string' && event.name.trim().length > 0) { return event.name; } if (event.name && typeof event.name === 'object') { const lang = i18n.language?.split('-')[0] ?? 'de'; return ( event.name[lang] ?? event.name.de ?? event.name.en ?? Object.values(event.name)[0] ?? fallback ); } return fallback; }, [i18n.language, t] ); 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 (force = false) => { setLoading(true); setError(null); try { const [packagesResult, paddleTransactions, addonHistoryResult] = await Promise.all([ getTenantPackagesOverview(force ? { force: true } : undefined), getTenantPaddleTransactions().catch((err) => { console.warn('Failed to load Paddle transactions', err); return { data: [] as PaddleTransactionSummary[], nextCursor: null, hasMore: false }; }), getTenantAddonHistory().catch((err) => { console.warn('Failed to load add-on history', err); return { data: [] as TenantAddonHistoryEntry[], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } }; }), ]); setPackages(packagesResult.packages); setActivePackage(packagesResult.activePackage); setTransactions(paddleTransactions.data); setTransactionCursor(paddleTransactions.nextCursor); setTransactionsHasMore(paddleTransactions.hasMore); setAddonHistory(addonHistoryResult.data); setAddonMeta(addonHistoryResult.meta); } 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]); const loadMoreAddons = React.useCallback(async () => { if (addonsLoading || !addonMeta || addonMeta.current_page >= addonMeta.last_page) { return; } setAddonsLoading(true); try { const nextPage = addonMeta.current_page + 1; const result = await getTenantAddonHistory(nextPage); setAddonHistory((current) => [...current, ...result.data]); setAddonMeta(result.meta); } catch (error) { console.warn('Failed to load additional add-on history', error); } finally { setAddonsLoading(false); } }, [addonMeta, addonsLoading]); React.useEffect(() => { void loadAll(); }, [loadAll]); const hasMoreAddons = React.useMemo(() => { if (!addonMeta) { return false; } return addonMeta.current_page < addonMeta.last_page; }, [addonMeta]); const computedRemainingEvents = React.useMemo(() => { if (!activePackage) { return null; } const used = activePackage.used_events ?? 0; if (activePackage.remaining_events !== null && activePackage.remaining_events !== undefined) { return activePackage.remaining_events; } const allowance = activePackage.package_limits?.max_events_per_year ?? 1; return Math.max(0, allowance - used); }, [activePackage]); const normalizedActivePackage = React.useMemo(() => { if (!activePackage) return null; return { ...activePackage, remaining_events: computedRemainingEvents ?? activePackage.remaining_events, }; }, [activePackage, computedRemainingEvents]); const topWarning = React.useMemo(() => { const warnings = buildPackageWarnings( normalizedActivePackage, (key, options) => t(key, options), formatDate, 'billing.sections.overview.warnings', ); return warnings[0]; }, [formatDate, normalizedActivePackage, t]); const activeWarnings = React.useMemo( () => buildPackageWarnings(normalizedActivePackage, t, formatDate, 'billing.sections.overview.warnings'), [normalizedActivePackage, t, formatDate], ); const billingStats = React.useMemo(() => { if (!activePackage) { return [] as const; } const used = activePackage.used_events ?? 0; const remaining = computedRemainingEvents ?? 0; return [ { key: 'events', label: t('billing.stats.events.label', 'Genutzte Events'), value: used, helper: t('billing.stats.events.helper', { count: remaining }), tone: 'amber' as const, }, ]; }, [activePackage, computedRemainingEvents, t]); return ( {error && ( {t('dashboard:alerts.errorTitle')} {error} )} {loading ? ( ) : ( <> {activePackage ? activePackage.package_name : t('billing.sections.overview.emptyBadge')} )} /> {activePackage ? (
) : ( )}
{packages.length === 0 ? ( ) : ( packages.map((pkg) => { const warnings = buildPackageWarnings(pkg, t, formatDate, 'billing.sections.packages.card.warnings'); return ( ); }) )}
{addonHistory.length === 0 ? ( ) : ( )} {hasMoreAddons && ( )}
{transactions.length === 0 ? ( ) : ( )} {transactionsHasMore && ( )}
)}
); } function AddonHistoryTable({ items, formatCurrency, formatDate, resolveEventName, locale, t, }: { items: TenantAddonHistoryEntry[]; formatCurrency: (value: number | null | undefined, currency?: string) => string; formatDate: (value: string | null | undefined) => string; resolveEventName: (event: TenantAddonHistoryEntry['event']) => string; locale: string; t: (key: string, options?: Record) => string; }) { const extrasLabel = (key: 'photos' | 'guests' | 'gallery', count: number) => t(`billing.sections.addOns.extras.${key}`, { count }); return ( {items.map((item) => { const extras: string[] = []; if (item.extra_photos > 0) { extras.push(extrasLabel('photos', item.extra_photos)); } if (item.extra_guests > 0) { extras.push(extrasLabel('guests', item.extra_guests)); } if (item.extra_gallery_days > 0) { extras.push(extrasLabel('gallery', item.extra_gallery_days)); } const purchasedLabel = item.purchased_at ? new Date(item.purchased_at).toLocaleString(locale, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', }) : formatDate(item.purchased_at); const statusKey = `billing.sections.addOns.status.${item.status}`; const statusLabel = t(statusKey, { defaultValue: item.status }); const statusTone: Record = { completed: 'bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200', pending: 'bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200', failed: 'bg-rose-500/15 text-rose-700 dark:bg-rose-500/20 dark:text-rose-200', }; return ( ); })}
{t('billing.sections.addOns.table.addon')} {t('billing.sections.addOns.table.event')} {t('billing.sections.addOns.table.amount')} {t('billing.sections.addOns.table.status')} {t('billing.sections.addOns.table.purchased')}
{item.label ?? item.addon_key} {item.quantity > 1 ? ( ×{item.quantity} ) : null}
{extras.length > 0 ? (

{extras.join(' · ')}

) : null}

{resolveEventName(item.event)}

{item.event?.slug ? (

{item.event.slug}

) : null}

{formatCurrency(item.amount, item.currency ?? 'EUR')}

{item.receipt_url ? ( {t('billing.sections.transactions.labels.receipt')} ) : null}
{statusLabel} {purchasedLabel}
); } function TransactionsTable({ items, formatCurrency, formatDate, locale, t, }: { items: PaddleTransactionSummary[]; formatCurrency: (value: number | null | undefined, currency?: string) => string; formatDate: (value: string | null | undefined) => string; locale: string; t: (key: string, options?: Record) => string; }) { const statusTone: Record = { completed: 'bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200', processing: 'bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200', failed: 'bg-rose-500/15 text-rose-700 dark:bg-rose-500/20 dark:text-rose-200', cancelled: 'bg-slate-200 text-slate-700 dark:bg-slate-700/40 dark:text-slate-200', }; return ( {items.map((transaction) => { const amount = transaction.grand_total ?? transaction.amount ?? null; const statusKey = transaction.status ? `billing.sections.transactions.status.${transaction.status}` : 'billing.sections.transactions.status.unknown'; const statusLabel = t(statusKey, { defaultValue: transaction.status ?? 'Unknown' }); const createdAt = transaction.created_at ? new Date(transaction.created_at).toLocaleString(locale, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', }) : formatDate(transaction.created_at); return ( ); })}
{t('billing.sections.transactions.table.transaction', 'Transaktion')} {t('billing.sections.transactions.table.amount', 'Betrag')} {t('billing.sections.transactions.table.status', 'Status')} {t('billing.sections.transactions.table.date', 'Datum')} {t('billing.sections.transactions.table.origin', 'Herkunft')}

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

{transaction.checkout_id ? (

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

) : null}

{formatCurrency(amount, transaction.currency ?? 'EUR')}

{transaction.tax ? (

{t('billing.sections.transactions.labels.tax', { value: formatCurrency(transaction.tax, transaction.currency ?? 'EUR') })}

) : null} {transaction.receipt_url ? ( {t('billing.sections.transactions.labels.receipt', 'Beleg ansehen')} ) : null}
{statusLabel} {createdAt}

{transaction.origin ?? '—'}

); } function BillingStatGrid({ stats, }: { stats: Array<{ key: string; label: string; value: string | number | null | undefined; helper?: string; tone: 'pink' | 'amber' | 'sky' | 'emerald' }>; }) { return (
{stats.map((stat) => ( ))}
); } function BillingWarningBanner({ warnings, t }: { warnings: PackageWarning[]; t: (key: string, options?: Record) => string }) { if (!warnings.length) { return null; } return ( {t('billingWarning.title', 'Handlungsbedarf')}

{t('billingWarning.description', 'Paketwarnungen und Limits, die du im Blick behalten solltest.')}

    {warnings.map((warning) => (
  • {warning.message}
  • ))}
); } function InfoCard({ label, value, helper, tone, }: { label: string; value: string | number | null | undefined; helper?: string; tone: 'pink' | 'amber' | 'sky' | 'emerald'; }) { const toneBorders: Record<'pink' | 'amber' | 'sky' | 'emerald', string> = { pink: 'border-pink-200/60 shadow-rose-200/30', amber: 'border-amber-200/60 shadow-amber-200/30', sky: 'border-sky-200/60 shadow-sky-200/30', emerald: 'border-emerald-200/60 shadow-emerald-200/30', } as const; return ( {label}
{value ?? '--'}
{helper ?

{helper}

: null}
); } function PackageCard({ pkg, isActive, labels, formatDate, formatCurrency, warnings = [], }: { 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; warnings?: PackageWarning[]; }) { return ( {warnings.length > 0 && (
{warnings.map((warning) => ( {warning.message} ))}
)}

{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 buildPackageWarnings( pkg: TenantPackageSummary | null | undefined, translate: (key: string, options?: Record) => string, formatDate: (value: string | null | undefined) => string, keyPrefix: string, ): PackageWarning[] { if (!pkg) { return []; } const warnings: PackageWarning[] = []; const remaining = typeof pkg.remaining_events === 'number' ? pkg.remaining_events : null; const allowance = pkg.package_limits?.max_events_per_year; const used = pkg.used_events ?? 0; const totalEvents = allowance ?? (remaining !== null ? remaining + used : null); // Warnungen nur, wenn das Paket tatsächlich mehr als 1 Event umfasst oder das Limit unbekannt ist. const shouldWarn = totalEvents === null ? true : totalEvents > 1; if (remaining !== null && shouldWarn) { if (remaining <= 0) { warnings.push({ id: `${pkg.id}-no-events`, tone: 'danger', message: translate(`${keyPrefix}.noEvents`), }); } else if (remaining <= 2) { warnings.push({ id: `${pkg.id}-low-events`, tone: 'warning', message: translate(`${keyPrefix}.lowEvents`, { remaining }), }); } } const expiresAt = pkg.expires_at ? new Date(pkg.expires_at) : null; if (expiresAt && !Number.isNaN(expiresAt.getTime())) { const now = new Date(); const diffMillis = expiresAt.getTime() - now.getTime(); const diffDays = Math.ceil(diffMillis / (1000 * 60 * 60 * 24)); const formatted = formatDate(pkg.expires_at); if (diffDays < 0) { warnings.push({ id: `${pkg.id}-expired`, tone: 'danger', message: translate(`${keyPrefix}.expired`, { date: formatted }), }); } else if (diffDays <= 14) { warnings.push({ id: `${pkg.id}-expires`, tone: 'warning', message: translate(`${keyPrefix}.expiresSoon`, { date: formatted }), }); } } return warnings; } function BillingSkeleton() { return (
{Array.from({ length: 3 }).map((_, index) => (
{Array.from({ length: 4 }).map((__, placeholderIndex) => (
))}
))}
); }