Update admin PWA events, branding, and packages
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user