import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { format } from 'date-fns'; import { de, enGB } from 'date-fns/locale'; import { Layers, Library, Loader2, Plus } from 'lucide-react'; import { AdminLayout } from '../components/AdminLayout'; import { getTaskCollections, importTaskCollection, getEvents, PaginationMeta, TenantEvent, TenantTaskCollection, } from '../api'; import { ADMIN_TASKS_PATH } from '../constants'; import { isAuthError } from '../auth/tokens'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; const DEFAULT_PAGE_SIZE = 12; type ScopeFilter = 'all' | 'global' | 'tenant'; type CollectionsState = { items: TenantTaskCollection[]; meta: PaginationMeta | null; }; export default function TaskCollectionsPage(): JSX.Element { const navigate = useNavigate(); const { t, i18n } = useTranslation('management'); const [collectionsState, setCollectionsState] = React.useState({ items: [], meta: null }); const [page, setPage] = React.useState(1); const [search, setSearch] = React.useState(''); const [scope, setScope] = React.useState('all'); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [successMessage, setSuccessMessage] = React.useState(null); const [dialogOpen, setDialogOpen] = React.useState(false); const [selectedCollection, setSelectedCollection] = React.useState(null); const [events, setEvents] = React.useState([]); const [selectedEventSlug, setSelectedEventSlug] = React.useState(''); const [importing, setImporting] = React.useState(false); const [eventsLoading, setEventsLoading] = React.useState(false); const [eventError, setEventError] = React.useState(null); const [reloadToken, setReloadToken] = React.useState(0); const scopeParam = React.useMemo(() => { if (scope === 'global') return 'global'; if (scope === 'tenant') return 'tenant'; return undefined; }, [scope]); React.useEffect(() => { let cancelled = false; async function loadCollections() { setLoading(true); setError(null); try { const result = await getTaskCollections({ page, per_page: DEFAULT_PAGE_SIZE, search: search.trim() || undefined, scope: scopeParam, }); if (cancelled) return; setCollectionsState({ items: result.data, meta: result.meta }); } catch (err) { if (cancelled) return; if (!isAuthError(err)) { setError(t('collections.notifications.error')); } } finally { if (!cancelled) { setLoading(false); } } } void loadCollections(); return () => { cancelled = true; }; }, [page, search, scopeParam, reloadToken, t]); React.useEffect(() => { if (successMessage) { const timeout = setTimeout(() => setSuccessMessage(null), 4000); return () => clearTimeout(timeout); } return undefined; }, [successMessage]); async function ensureEventsLoaded() { if (events.length > 0 || eventsLoading) { return; } setEventsLoading(true); setEventError(null); try { const result = await getEvents(); setEvents(result); } catch (err) { if (!isAuthError(err)) { setEventError(t('collections.errors.eventsLoad')); } } finally { setEventsLoading(false); } } function openImportDialog(collection: TenantTaskCollection) { setSelectedCollection(collection); setSelectedEventSlug(''); setDialogOpen(true); void ensureEventsLoaded(); } async function handleImport(event: React.FormEvent) { event.preventDefault(); if (!selectedCollection || !selectedEventSlug) { setEventError(t('collections.errors.selectEvent')); return; } setImporting(true); setEventError(null); try { await importTaskCollection(selectedCollection.id, selectedEventSlug); setSuccessMessage(t('collections.notifications.imported')); setDialogOpen(false); setReloadToken((token) => token + 1); } catch (err) { if (!isAuthError(err)) { setEventError(t('collections.notifications.error')); } } finally { setImporting(false); } } const showEmpty = !loading && collectionsState.items.length === 0; return ( } > {error && ( {t('collections.notifications.error')} {error} )} {successMessage && ( {t('collections.notifications.imported')} {successMessage} )} {t('collections.title')} {t('collections.subtitle')}
{ setPage(1); setSearch(event.target.value); }} placeholder={t('collections.filters.search') ?? 'Search collections'} className="w-full lg:max-w-md" />
{loading ? ( ) : showEmpty ? ( navigate(ADMIN_TASKS_PATH)} /> ) : (
{collectionsState.items.map((collection) => ( openImportDialog(collection)} /> ))}
)} {collectionsState.meta && collectionsState.meta.last_page > 1 && (
{t('collections.pagination.page', { current: collectionsState.meta.current_page, total: collectionsState.meta.last_page, })}
)}
); } function CollectionCard({ collection, onImport, }: { collection: TenantTaskCollection; onImport: () => void; }) { const { t, i18n } = useTranslation('management'); const locale = i18n.language.startsWith('en') ? enGB : de; const updatedLabel = collection.updated_at ? format(new Date(collection.updated_at), 'PP', { locale }) : null; const scopeLabel = collection.is_global ? t('collections.scope.global') : t('collections.scope.tenant'); const eventTypeLabel = collection.event_type?.name ?? t('collections.filters.allEventTypes'); return (
{scopeLabel} {eventTypeLabel && ( {eventTypeLabel} )}
{collection.name} {collection.description && ( {collection.description} )}
{t('collections.labels.taskCount', { count: collection.tasks_count })}
{updatedLabel && {t('collections.labels.updated', { date: updatedLabel })}}
); } function EmptyCollectionsState({ onCreate }: { onCreate: () => void }) { const { t } = useTranslation('management'); return (

{t('collections.empty.title')}

{t('collections.empty.description')}

); } function CollectionSkeleton() { return (
{Array.from({ length: 4 }).map((_, index) => (
))}
); } function ImportDialog({ open, onOpenChange, collection, events, eventsLoading, eventError, selectedEventSlug, onEventChange, onSubmit, importing, }: { open: boolean; onOpenChange: (open: boolean) => void; collection: TenantTaskCollection | null; events: TenantEvent[]; eventsLoading: boolean; eventError: string | null; selectedEventSlug: string; onEventChange: (value: string) => void; onSubmit: (event: React.FormEvent) => void; importing: boolean; }) { const { t, i18n } = useTranslation('management'); return ( {t('collections.dialogs.importTitle')}
{events.length === 0 && !eventsLoading && (

{t('collections.errors.noEvents') ?? 'Noch keine Events vorhanden.'}

)} {eventError &&

{eventError}

}
); } function formatEventLabel(event: TenantEvent, language: string): string { const locales = [language, language?.split('-')[0], 'de', 'en'].filter(Boolean) as string[]; let name: string | undefined; if (typeof event.name === 'string') { name = event.name; } else if (event.name && typeof event.name === 'object') { for (const locale of locales) { const value = (event.name as Record)[locale!]; if (value) { name = value; break; } } if (!name) { const first = Object.values(event.name as Record)[0]; if (first) { name = first; } } } const eventDate = event.event_date ? new Date(event.event_date) : null; if (!eventDate) { return name ?? event.slug; } const locale = language.startsWith('en') ? enGB : de; return `${name ?? event.slug} (${format(eventDate, 'PP', { locale })})`; }