neues Admin UI Layout eingeführt. Alle Tests auf den neusten Stand gebracht.

This commit is contained in:
Codex Agent
2025-12-30 10:24:06 +01:00
parent 902e78cae9
commit efe2f25b3e
85 changed files with 95235 additions and 19197 deletions

View File

@@ -20,11 +20,13 @@ import { getApiErrorMessage } from '../lib/apiError';
import { ADMIN_EVENT_VIEW_PATH, adminPath } from '../constants';
import { buildPackageUsageMetrics, PackageUsageMetric, usagePercent } from './billingUsage';
import { useBackNavigation } from './hooks/useBackNavigation';
import { useAdminTheme } from './theme';
export default function MobileBillingPage() {
const { t } = useTranslation('management');
const navigate = useNavigate();
const location = useLocation();
const { textStrong, text, muted, subtle, danger, border, primary, accentSoft } = useAdminTheme();
const [packages, setPackages] = React.useState<TenantPackageSummary[]>([]);
const [activePackage, setActivePackage] = React.useState<TenantPackageSummary | null>(null);
const [transactions, setTransactions] = React.useState<PaddleTransactionSummary[]>([]);
@@ -107,13 +109,13 @@ export default function MobileBillingPage() {
onBack={back}
headerActions={
<HeaderActionButton onPress={() => load()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color="#0f172a" />
<RefreshCcw size={18} color={textStrong} />
</HeaderActionButton>
}
>
{error ? (
<MobileCard>
<Text fontWeight="700" color="#b91c1c">
<Text fontWeight="700" color={danger}>
{error}
</Text>
<CTAButton label={t('billing.actions.refresh', 'Refresh')} tone="ghost" onPress={load} />
@@ -122,12 +124,12 @@ export default function MobileBillingPage() {
<MobileCard space="$2" ref={packagesRef as any}>
<XStack alignItems="center" space="$2">
<Package size={18} color="#0f172a" />
<Text fontSize="$md" fontWeight="800" color="#0f172a">
<Package size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('billing.sections.packages.title', 'Packages')}
</Text>
</XStack>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('billing.sections.packages.hint', 'Active package, limits, and history at a glance.')}
</Text>
<CTAButton
@@ -136,7 +138,7 @@ export default function MobileBillingPage() {
disabled={portalBusy}
/>
{loading ? (
<Text fontSize="$sm" color="#6b7280">
<Text fontSize="$sm" color={muted}>
{t('common.loading', 'Lädt...')}
</Text>
) : (
@@ -159,21 +161,21 @@ export default function MobileBillingPage() {
<MobileCard space="$2" ref={invoicesRef as any}>
<XStack alignItems="center" space="$2">
<Receipt size={18} color="#0f172a" />
<Text fontSize="$md" fontWeight="800" color="#0f172a">
<Receipt size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('billing.sections.invoices.title', 'Invoices & Payments')}
</Text>
</XStack>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('billing.sections.invoices.hint', 'Review transactions and download receipts.')}
</Text>
{loading ? (
<Text fontSize="$sm" color="#6b7280">
<Text fontSize="$sm" color={muted}>
{t('common.loading', 'Lädt...')}
</Text>
) : transactions.length === 0 ? (
<YStack space="$2">
<Text fontSize="$sm" color="#4b5563">
<Text fontSize="$sm" color={text}>
{t('billing.sections.invoices.empty', 'Keine Zahlungen gefunden.')}
</Text>
<CTAButton label={t('billing.actions.openPackages', 'Open packages')} onPress={scrollToPackages} />
@@ -182,31 +184,31 @@ export default function MobileBillingPage() {
) : (
<YStack space="$1.5">
{transactions.slice(0, 8).map((trx) => (
<XStack key={trx.id} alignItems="center" justifyContent="space-between" borderBottomWidth={1} borderColor="#e5e7eb" paddingVertical="$1.5">
<XStack key={trx.id} alignItems="center" justifyContent="space-between" borderBottomWidth={1} borderColor={border} paddingVertical="$1.5">
<YStack>
<Text fontSize="$sm" color="#0f172a" fontWeight="700">
<Text fontSize="$sm" color={textStrong} fontWeight="700">
{trx.status ?? '—'}
</Text>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{formatDate(trx.created_at)}
</Text>
{trx.origin ? (
<Text fontSize="$xs" color="#9ca3af">
<Text fontSize="$xs" color={subtle}>
{trx.origin}
</Text>
) : null}
</YStack>
<YStack alignItems="flex-end">
<Text fontSize="$sm" color="#0f172a" fontWeight="700">
<Text fontSize="$sm" color={textStrong} fontWeight="700">
{formatAmount(trx.amount, trx.currency)}
</Text>
{trx.tax ? (
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('billing.sections.transactions.labels.tax', { value: formatAmount(trx.tax, trx.currency) })}
</Text>
) : null}
{trx.receipt_url ? (
<a href={trx.receipt_url} target="_blank" rel="noreferrer" style={{ fontSize: 12, color: '#2563eb' }}>
<a href={trx.receipt_url} target="_blank" rel="noreferrer" style={{ fontSize: 12, color: primary }}>
{t('billing.sections.transactions.labels.receipt', 'Beleg')}
</a>
) : null}
@@ -220,20 +222,20 @@ export default function MobileBillingPage() {
<MobileCard space="$2">
<XStack alignItems="center" space="$2">
<Sparkles size={18} color="#0f172a" />
<Text fontSize="$md" fontWeight="800" color="#0f172a">
<Sparkles size={18} color={textStrong} />
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{t('billing.sections.addOns.title', 'Add-ons')}
</Text>
</XStack>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{t('billing.sections.addOns.hint', 'Track extra photo, guest, or time bundles per event.')}
</Text>
{loading ? (
<Text fontSize="$sm" color="#6b7280">
<Text fontSize="$sm" color={muted}>
{t('common.loading', 'Lädt...')}
</Text>
) : addons.length === 0 ? (
<Text fontSize="$sm" color="#4b5563">
<Text fontSize="$sm" color={text}>
{t('billing.sections.addOns.empty', 'Keine Add-ons gebucht.')}
</Text>
) : (
@@ -251,19 +253,25 @@ export default function MobileBillingPage() {
function PackageCard({ pkg, label, isActive = false }: { pkg: TenantPackageSummary; label?: string; isActive?: boolean }) {
const { t } = useTranslation('management');
const { border, primary, accentSoft, textStrong, muted } = useAdminTheme();
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;
const usageMetrics = buildPackageUsageMetrics(pkg);
return (
<MobileCard borderColor={isActive ? '#2563eb' : '#e5e7eb'} borderWidth={isActive ? 2 : 1} backgroundColor={isActive ? '#eff6ff' : undefined} space="$2">
<MobileCard
borderColor={isActive ? primary : border}
borderWidth={isActive ? 2 : 1}
backgroundColor={isActive ? accentSoft : undefined}
space="$2"
>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$md" fontWeight="800" color="#0f172a">
<Text fontSize="$md" fontWeight="800" color={textStrong}>
{pkg.package_name ?? t('mobileBilling.packageFallback', 'Package')}
</Text>
{label ? <PillBadge tone="success">{label}</PillBadge> : null}
</XStack>
{expires ? (
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{expires}
</Text>
) : null}
@@ -297,6 +305,7 @@ function renderFeatureBadge(pkg: TenantPackageSummary, t: any, key: string, labe
function UsageBar({ metric }: { metric: PackageUsageMetric }) {
const { t } = useTranslation('management');
const { muted, textStrong, border, primary, subtle } = useAdminTheme();
const labelMap: Record<PackageUsageMetric['key'], string> = {
events: t('mobileBilling.usage.events', 'Events'),
guests: t('mobileBilling.usage.guests', 'Guests'),
@@ -319,18 +328,18 @@ function UsageBar({ metric }: { metric: PackageUsageMetric }) {
return (
<YStack space="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{labelMap[metric.key]}
</Text>
<Text fontSize="$xs" color="#0f172a" fontWeight="700">
<Text fontSize="$xs" color={textStrong} fontWeight="700">
{valueText}
</Text>
</XStack>
<YStack height={6} borderRadius={999} backgroundColor="#e5e7eb" overflow="hidden">
<YStack height="100%" width={`${fill}%`} backgroundColor={hasUsage ? '#2563eb' : '#94a3b8'} />
<YStack height={6} borderRadius={999} backgroundColor={border} overflow="hidden">
<YStack height="100%" width={`${fill}%`} backgroundColor={hasUsage ? primary : subtle} />
</YStack>
{remainingText ? (
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{remainingText}
</Text>
) : null}
@@ -353,6 +362,7 @@ function formatAmount(value: number | null | undefined, currency: string | null
function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
const { t } = useTranslation('management');
const navigate = useNavigate();
const { border, textStrong, text, muted, subtle, primary } = useAdminTheme();
const labels: Record<TenantAddonHistoryEntry['status'], { tone: 'success' | 'warning' | 'muted'; text: string }> = {
completed: { tone: 'success', text: t('mobileBilling.status.completed', 'Completed') },
pending: { tone: 'warning', text: t('mobileBilling.status.pending', 'Pending') },
@@ -380,9 +390,9 @@ function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
) : null;
return (
<MobileCard borderColor="#e5e7eb" padding="$3" space="$1.5">
<MobileCard borderColor={border} padding="$3" space="$1.5">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" fontWeight="700" color="#0f172a">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{addon.label ?? addon.addon_key}
</Text>
<PillBadge tone={status.tone}>{status.text}</PillBadge>
@@ -391,25 +401,25 @@ function AddonRow({ addon }: { addon: TenantAddonHistoryEntry }) {
eventPath ? (
<Pressable onPress={() => navigate(eventPath)}>
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$xs" color="#0f172a" fontWeight="600">
<Text fontSize="$xs" color={textStrong} fontWeight="600">
{eventName}
</Text>
<Text fontSize="$xs" color="#2563eb" fontWeight="700">
<Text fontSize="$xs" color={primary} fontWeight="700">
{t('mobileBilling.openEvent', 'Open event')}
</Text>
</XStack>
</Pressable>
) : (
<Text fontSize="$xs" color="#9ca3af">
<Text fontSize="$xs" color={subtle}>
{eventName}
</Text>
)
) : null}
{impactBadges}
<Text fontSize="$sm" color="#0f172a" marginTop="$1.5">
<Text fontSize="$sm" color={text} marginTop="$1.5">
{formatAmount(addon.amount, addon.currency)}
</Text>
<Text fontSize="$xs" color="#6b7280">
<Text fontSize="$xs" color={muted}>
{formatDate(addon.purchased_at)}
</Text>
</MobileCard>