neues Admin UI Layout eingeführt. Alle Tests auf den neusten Stand gebracht.
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user