import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { CreditCard, Package, Receipt, RefreshCcw, Sparkles } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge } from './components/Primitives'; import { getTenantPackagesOverview, getTenantPaddleTransactions, TenantPackageSummary, PaddleTransactionSummary, } from '../api'; import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api'; import { getApiErrorMessage } from '../lib/apiError'; import { adminPath } from '../constants'; export default function MobileBillingPage() { const { t } = useTranslation('management'); const navigate = useNavigate(); const location = useLocation(); const [packages, setPackages] = React.useState([]); const [activePackage, setActivePackage] = React.useState(null); const [transactions, setTransactions] = React.useState([]); const [addons, setAddons] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const packagesRef = React.useRef(null); const invoicesRef = React.useRef(null); const load = React.useCallback(async () => { setLoading(true); try { const [pkg, trx, addonHistory] = await Promise.all([ getTenantPackagesOverview({ force: true }), getTenantPaddleTransactions().catch(() => ({ data: [] as PaddleTransactionSummary[] })), getTenantAddonHistory().catch(() => ({ data: [] as TenantAddonHistoryEntry[] })), ]); setPackages(pkg.packages ?? []); setActivePackage(pkg.activePackage ?? null); setTransactions(trx.data ?? []); setAddons(addonHistory.data ?? []); setError(null); } catch (err) { const message = getApiErrorMessage(err, t('billing.errors.load', 'Konnte Abrechnung nicht laden.')); setError(message); } finally { setLoading(false); } }, [t]); React.useEffect(() => { void load(); }, [load]); React.useEffect(() => { if (!location.hash) return; const hash = location.hash.replace('#', ''); const target = hash === 'invoices' ? invoicesRef.current : packagesRef.current; if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, [location.hash, loading]); return ( navigate(-1)} headerActions={ load()}> } > {error ? ( {error} ) : null} {t('billing.sections.packages.title', 'Packages')} {loading ? ( {t('common.loading', 'Lädt...')} ) : ( {activePackage ? ( ) : null} {packages .filter((pkg) => !activePackage || pkg.id !== activePackage.id) .map((pkg) => ( ))} )} {t('billing.sections.invoices.title', 'Invoices & Payments')} {loading ? ( {t('common.loading', 'Lädt...')} ) : transactions.length === 0 ? ( {t('billing.sections.invoices.empty', 'Keine Zahlungen gefunden.')} ) : ( {transactions.slice(0, 8).map((trx) => ( {trx.status ?? '—'} {formatDate(trx.created_at)} {trx.origin ? ( {trx.origin} ) : null} {formatAmount(trx.amount, trx.currency)} {trx.tax ? ( {t('billing.sections.transactions.labels.tax', { value: formatAmount(trx.tax, trx.currency) })} ) : null} {trx.receipt_url ? ( {t('billing.sections.transactions.labels.receipt', 'Beleg')} ) : null} ))} )} {null} {t('billing.sections.addOns.title', 'Add-ons')} {loading ? ( {t('common.loading', 'Lädt...')} ) : addons.length === 0 ? ( {t('billing.sections.addOns.empty', 'Keine Add-ons gebucht.')} ) : ( {addons.slice(0, 8).map((addon) => ( ))} )} {null} ); } function PackageCard({ pkg, label }: { pkg: TenantPackageSummary; label?: string }) { const { t } = useTranslation('management'); const remaining = pkg.remaining_events ?? (pkg.package_limits?.max_events_per_year as number | undefined) ?? 0; const expires = pkg.expires_at ? formatDate(pkg.expires_at) : null; return ( {pkg.package_name ?? t('mobileBilling.packageFallback', 'Package')} {label ? {label} : null} {expires ? ( {expires} ) : null} {t('mobileBilling.remainingEvents', '{{count}} events', { count: remaining })} {pkg.price !== null && pkg.price !== undefined ? ( {formatAmount(pkg.price, pkg.currency ?? 'EUR')} ) : null} {renderFeatureBadge(pkg, t, 'branding_allowed', t('billing.features.branding', 'Branding'))} {renderFeatureBadge(pkg, t, 'watermark_allowed', t('billing.features.watermark', 'Watermark'))} ); } function renderFeatureBadge(pkg: TenantPackageSummary, t: any, key: string, label: string) { const value = (pkg.package_limits as any)?.[key] ?? (pkg as any)[key]; if (value === undefined || value === null) return null; const enabled = value !== false; return {enabled ? label : `${label} off`}; } function FeatureList({ pkg }: { pkg: TenantPackageSummary }) { const { t } = useTranslation('management'); const limits = pkg.package_limits ?? {}; const features = (pkg as any).features as string[] | undefined; const rows: Array<{ label: string; value: string }> = []; if (limits.max_photos !== undefined && limits.max_photos !== null) { rows.push({ label: t('billing.features.maxPhotos', 'Max photos'), value: String(limits.max_photos) }); } if (limits.max_guests !== undefined && limits.max_guests !== null) { rows.push({ label: t('billing.features.maxGuests', 'Max guests'), value: String(limits.max_guests) }); } if (limits.gallery_days !== undefined && limits.gallery_days !== null) { rows.push({ label: t('billing.features.galleryDays', 'Gallery days'), value: String(limits.gallery_days) }); } if (limits.max_tasks !== undefined && limits.max_tasks !== null) { rows.push({ label: t('billing.features.maxTasks', 'Max tasks'), value: String(limits.max_tasks) }); } if (Array.isArray(features) && features.length) { rows.push({ label: t('billing.features.featureList', 'Included features'), value: features.join(', ') }); } if (!rows.length) return null; return ( {rows.map((row) => ( {row.label} {row.value} ))} ); } function formatAmount(value: number | null | undefined, currency: string | null | undefined): string { if (value === null || value === undefined) { return '—'; } const cur = currency ?? 'EUR'; try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: cur }).format(value); } catch { return `${value} ${cur}`; } } function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) { const { t } = useTranslation('management'); const labels: Record = { completed: { tone: 'success', text: t('mobileBilling.status.completed', 'Completed') }, pending: { tone: 'warning', text: t('mobileBilling.status.pending', 'Pending') }, failed: { tone: 'muted', text: t('mobileBilling.status.failed', 'Failed') }, }; const status = labels[addon.status]; const eventName = (addon.event?.name && typeof addon.event.name === 'string' && addon.event.name) || (addon.event?.name && typeof addon.event.name === 'object' ? addon.event.name?.en ?? addon.event.name?.de ?? Object.values(addon.event.name)[0] : null) || null; return ( {addon.label ?? addon.addon_key} {status.text} {formatDate(addon.purchased_at)} {eventName ? ( {eventName} ) : null} {addon.extra_photos ? ( {t('mobileBilling.extra.photos', '+{{count}} photos', { count: addon.extra_photos })} ) : null} {addon.extra_guests ? ( {t('mobileBilling.extra.guests', '+{{count}} guests', { count: addon.extra_guests })} ) : null} {addon.extra_gallery_days ? ( {t('mobileBilling.extra.days', '+{{count}} days', { count: addon.extra_gallery_days })} ) : null} {formatAmount(addon.amount, addon.currency)} ); } function formatDate(value: string | null | undefined): string { if (!value) return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '—'; return date.toLocaleDateString(undefined, { day: '2-digit', month: 'short', year: 'numeric' }); }