Update admin PWA events, branding, and packages
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-19 11:35:38 +01:00
parent 926bc7d070
commit fbff2afa3e
43 changed files with 6846 additions and 6323 deletions

View File

@@ -56,35 +56,69 @@ function formatEventName(event: TenantEvent | DataExportSummary['event'] | null)
return event.slug ?? '';
}
export default function MobileDataExportsPage() {
type DataExportsPanelProps = {
variant?: 'page' | 'recap';
event?: TenantEvent | null;
onRefreshReady?: (refresh: () => void) => void;
};
export function DataExportsPanel({
variant = 'page',
event,
onRefreshReady,
}: DataExportsPanelProps) {
const { t } = useTranslation('management');
const { textStrong, text, muted, danger } = useAdminTheme();
const [exports, setExports] = React.useState<DataExportSummary[]>([]);
const [events, setEvents] = React.useState<TenantEvent[]>([]);
const [scope, setScope] = React.useState<'tenant' | 'event'>('tenant');
const [eventId, setEventId] = React.useState<number | null>(null);
const isRecap = variant === 'recap';
const [scope, setScope] = React.useState<'tenant' | 'event'>(isRecap ? 'event' : 'tenant');
const [eventId, setEventId] = React.useState<number | null>(isRecap ? event?.id ?? null : null);
const [includeMedia, setIncludeMedia] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [requesting, setRequesting] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const back = useBackNavigation(adminPath('/mobile/profile'));
React.useEffect(() => {
if (!isRecap) {
return;
}
if (event?.id && eventId !== event.id) {
setEventId(event.id);
}
if (event) {
setEvents([event]);
}
}, [event, eventId, isRecap]);
const load = React.useCallback(async () => {
setLoading(true);
try {
const [exportRows, eventRows] = await Promise.all([
listTenantDataExports(),
getEvents({ force: true }),
isRecap ? Promise.resolve(event ? [event] : []) : getEvents({ force: true }),
]);
setExports(exportRows);
setEvents(eventRows);
if (!isRecap) {
setEvents(eventRows);
} else if (eventRows.length > 0) {
setEvents(eventRows);
}
setError(null);
} catch (err) {
setError(getApiErrorMessage(err, t('dataExports.errors.load', 'Exports konnten nicht geladen werden.')));
} finally {
setLoading(false);
}
}, [t]);
}, [event, isRecap, t]);
const refresh = React.useCallback(() => {
void load();
}, [load]);
React.useEffect(() => {
onRefreshReady?.(refresh);
}, [onRefreshReady, refresh]);
React.useEffect(() => {
void load();
@@ -111,7 +145,10 @@ export default function MobileDataExportsPage() {
return;
}
if (scope === 'event' && !eventId) {
const requestScope = isRecap ? 'event' : scope;
const requestEventId = isRecap ? eventId : eventId;
if (requestScope === 'event' && !requestEventId) {
setError(t('dataExports.errors.eventRequired', 'Bitte wähle ein Event aus.'));
return;
}
@@ -119,8 +156,8 @@ export default function MobileDataExportsPage() {
setRequesting(true);
try {
await requestTenantDataExport({
scope,
eventId: scope === 'event' ? eventId ?? undefined : undefined,
scope: requestScope,
eventId: requestScope === 'event' ? requestEventId ?? undefined : undefined,
includeMedia,
});
toast.success(t('dataExports.actions.requested', 'Export wird vorbereitet.'));
@@ -133,23 +170,24 @@ export default function MobileDataExportsPage() {
}
};
const visibleExports = React.useMemo(() => {
if (!isRecap) {
return exports;
}
if (!event?.id) {
return exports.filter((entry) => entry.scope === 'event');
}
return exports.filter((entry) => entry.scope === 'event' && entry.event?.id === event.id);
}, [event?.id, exports, isRecap]);
return (
<MobileShell
activeTab="profile"
title={t('dataExports.title', 'Data exports')}
onBack={back}
headerActions={
<HeaderActionButton onPress={() => load()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color={textStrong} />
</HeaderActionButton>
}
>
<>
{error ? (
<MobileCard>
<Text fontWeight="700" color={danger}>
{error}
</Text>
<CTAButton label={t('dataExports.actions.refresh', 'Refresh')} tone="ghost" onPress={load} />
<CTAButton label={t('dataExports.actions.refresh', 'Refresh')} tone="ghost" onPress={refresh} />
</MobileCard>
) : null}
@@ -161,21 +199,32 @@ export default function MobileDataExportsPage() {
{t('dataExports.request.hint', 'Export tenant data or a specific event archive.')}
</Text>
<YStack space="$2">
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" color={text}>
{t('dataExports.fields.scope', 'Scope')}
</Text>
<MobileSelect
value={scope}
onChange={(event) => setScope(event.target.value as 'tenant' | 'event')}
compact
style={{ minWidth: 140, maxWidth: 180 }}
>
<option value="tenant">{t('dataExports.scopes.tenant', 'Tenant')}</option>
<option value="event">{t('dataExports.scopes.event', 'Event')}</option>
</MobileSelect>
</XStack>
{scope === 'event' ? (
{!isRecap ? (
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" color={text}>
{t('dataExports.fields.scope', 'Scope')}
</Text>
<MobileSelect
value={scope}
onChange={(event) => setScope(event.target.value as 'tenant' | 'event')}
compact
style={{ minWidth: 140, maxWidth: 180 }}
>
<option value="tenant">{t('dataExports.scopes.tenant', 'Tenant')}</option>
<option value="event">{t('dataExports.scopes.event', 'Event')}</option>
</MobileSelect>
</XStack>
) : (
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" color={text}>
{t('dataExports.fields.event', 'Event')}
</Text>
<Text fontSize="$sm" color={textStrong}>
{formatEventName(event) || event?.slug || t('dataExports.fields.eventPlaceholder', 'Choose event')}
</Text>
</XStack>
)}
{!isRecap && scope === 'event' ? (
<XStack alignItems="center" justifyContent="space-between">
<Text fontSize="$sm" color={text}>
{t('dataExports.fields.event', 'Event')}
@@ -238,13 +287,13 @@ export default function MobileDataExportsPage() {
<SkeletonCard height={72} />
<SkeletonCard height={72} />
</YStack>
) : exports.length === 0 ? (
) : visibleExports.length === 0 ? (
<Text fontSize="$sm" color={muted}>
{t('dataExports.history.empty', 'No exports yet.')}
</Text>
) : (
<YStack space="$2">
{exports.map((entry) => (
{visibleExports.map((entry) => (
<MobileCard key={entry.id} space="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack>
@@ -301,6 +350,28 @@ export default function MobileDataExportsPage() {
</YStack>
)}
</MobileCard>
</>
);
}
export default function MobileDataExportsPage() {
const { t } = useTranslation('management');
const { textStrong } = useAdminTheme();
const back = useBackNavigation(adminPath('/mobile/profile'));
const [refresh, setRefresh] = React.useState<null | (() => void)>(null);
return (
<MobileShell
activeTab="profile"
title={t('dataExports.title', 'Data exports')}
onBack={back}
headerActions={
<HeaderActionButton onPress={() => refresh?.()} ariaLabel={t('common.refresh', 'Refresh')}>
<RefreshCcw size={18} color={textStrong} />
</HeaderActionButton>
}
>
<DataExportsPanel onRefreshReady={setRefresh} />
</MobileShell>
);
}