import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { AlertTriangle, ArrowLeft, Bell, Camera, CheckCircle2, Circle, Clock3, Loader2, MessageSquare, Printer, QrCode, RefreshCw, Smile, Sparkles, Users, } from 'lucide-react'; import toast from 'react-hot-toast'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { AdminLayout } from '../components/AdminLayout'; import { EventToolkit, EventToolkitTask, TenantEvent, TenantPhoto, EventStats, getEvent, getEventStats, getEventToolkit, toggleEvent, submitTenantFeedback, updatePhotoVisibility, } from '../api'; import { buildLimitWarnings } from '../lib/limitWarnings'; import { getApiErrorMessage } from '../lib/apiError'; import { isAuthError } from '../auth/tokens'; import { ADMIN_EVENTS_PATH, ADMIN_EVENT_EDIT_PATH, ADMIN_EVENT_INVITES_PATH, ADMIN_EVENT_MEMBERS_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENT_TASKS_PATH, } from '../constants'; import { SectionCard, SectionHeader, ActionGrid, TenantHeroCard, } from '../components/tenant'; import { GuestBroadcastCard } from '../components/GuestBroadcastCard'; type EventDetailPageProps = { mode?: 'detail' | 'toolkit'; }; type ToolkitState = { data: EventToolkit | null; loading: boolean; error: string | null; }; type WorkspaceState = { event: TenantEvent | null; stats: EventStats | null; loading: boolean; busy: boolean; error: string | null; }; export default function EventDetailPage({ mode = 'detail' }: EventDetailPageProps) { const { slug: slugParam } = useParams<{ slug?: string }>(); const navigate = useNavigate(); const { t } = useTranslation('management'); const { t: tCommon } = useTranslation('common'); const slug = slugParam ?? null; const [state, setState] = React.useState({ event: null, stats: null, loading: true, busy: false, error: null, }); const [toolkit, setToolkit] = React.useState({ data: null, loading: true, error: null }); const load = React.useCallback(async () => { if (!slug) { setState({ event: null, stats: null, loading: false, busy: false, error: t('events.errors.missingSlug', 'Kein Event ausgewählt.') }); setToolkit({ data: null, loading: false, error: null }); return; } setState((prev) => ({ ...prev, loading: true, error: null })); setToolkit((prev) => ({ ...prev, loading: true, error: null })); try { const [eventData, statsData] = await Promise.all([getEvent(slug), getEventStats(slug)]); setState((prev) => ({ ...prev, event: eventData, stats: statsData, loading: false })); } catch (error) { if (!isAuthError(error)) { setState((prev) => ({ ...prev, error: getApiErrorMessage(error, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.')), loading: false, })); } } try { const toolkitData = await getEventToolkit(slug); setToolkit({ data: toolkitData, loading: false, error: null }); } catch (error) { if (!isAuthError(error)) { setToolkit({ data: null, loading: false, error: getApiErrorMessage(error, t('toolkit.errors.loadFailed', 'Toolkit-Daten konnten nicht geladen werden.')), }); } } }, [slug, t]); React.useEffect(() => { void load(); }, [load]); async function handleToggle(): Promise { if (!slug) { return; } setState((prev) => ({ ...prev, busy: true, error: null })); try { const updated = await toggleEvent(slug); setState((prev) => ({ ...prev, busy: false, event: updated, stats: prev.stats ? { ...prev.stats, status: updated.status, is_active: Boolean(updated.is_active), } : prev.stats, })); } catch (error) { if (!isAuthError(error)) { setState((prev) => ({ ...prev, busy: false, error: getApiErrorMessage(error, t('events.errors.toggleFailed', 'Status konnte nicht angepasst werden.')), })); } else { setState((prev) => ({ ...prev, busy: false })); } } } const { event, stats, loading, busy, error } = state; const toolkitData = toolkit.data; const eventName = event ? resolveName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event'); const subtitle = mode === 'toolkit' ? t('events.workspace.toolkitSubtitle', 'Moderation, Aufgaben und Einladungen für deinen Eventtag bündeln.') : t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und Einladungen deines Events im Blick.'); const limitWarnings = React.useMemo( () => (event?.limits ? buildLimitWarnings(event.limits, (key, options) => tCommon(`limits.${key}`, options)) : []), [event?.limits, tCommon], ); const shownWarningToasts = React.useRef>(new Set()); React.useEffect(() => { limitWarnings.forEach((warning) => { const id = `${warning.id}-${warning.message}`; if (shownWarningToasts.current.has(id)) { return; } shownWarningToasts.current.add(id); toast(warning.message, { icon: warning.tone === 'danger' ? '🚨' : '⚠️', id, }); }); }, [limitWarnings]); if (!slug) { return (

{t('events.errors.notFoundBody', 'Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.')}

); } return ( {error && ( {t('events.alerts.failedTitle', 'Aktion fehlgeschlagen')} {error} )} {limitWarnings.length > 0 && (
{limitWarnings.map((warning) => ( {warning.message} ))}
)} {toolkit.error && ( {toolkit.error} )} {loading ? ( ) : event ? (
{ void load(); }} loading={state.busy} navigate={navigate} /> {(toolkitData?.alerts?.length ?? 0) > 0 && }
navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} /> navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} />
navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))} />
) : (

{t('events.errors.notFoundBody', 'Ohne gültige Kennung können wir keine Daten laden. Kehre zur Eventliste zurück und wähle dort ein Event aus.')}

)}
); } function resolveName(name: TenantEvent['name']): string { if (typeof name === 'string' && name.trim().length > 0) { return name.trim(); } if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? 'Event'; } return 'Event'; } function EventHeroCardSection({ event, stats, onRefresh, loading, navigate }: { event: TenantEvent; stats: EventStats | null; onRefresh: () => void; loading: boolean; navigate: ReturnType; }) { const { t } = useTranslation('management'); const statusLabel = getStatusLabel(event, t); const supporting = [ t('events.workspace.hero.status', { defaultValue: 'Status: {{status}}', status: statusLabel }), t('events.workspace.hero.date', { defaultValue: 'Eventdatum: {{date}}', date: formatDate(event.event_date) }), t('events.workspace.hero.metrics', { defaultValue: 'Uploads gesamt: {{count}} · Likes: {{likes}}', count: stats?.uploads_total ?? stats?.total ?? 0, likes: stats?.likes_total ?? stats?.likes ?? 0, }), ]; const aside = (
} label={t('events.workspace.fields.status', 'Status')} value={statusLabel} /> } label={t('events.workspace.fields.date', 'Eventdatum')} value={formatDate(event.event_date)} /> } label={t('events.workspace.fields.active', 'Aktiv für Gäste')} value={event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')} />
); return ( navigate(ADMIN_EVENTS_PATH)} className="rounded-full border-pink-200 text-pink-600 hover:bg-pink-50"> {t('events.actions.backToList', 'Zurück zur Liste')} )} secondaryAction={( )} aside={aside} >
); } function StatusCard({ event, stats, busy, onToggle }: { event: TenantEvent; stats: EventStats | null; busy: boolean; onToggle: () => void }) { const { t } = useTranslation('management'); const statusLabel = getStatusLabel(event, t); return (
} label={t('events.workspace.fields.status', 'Status')} value={statusLabel} /> } label={t('events.workspace.fields.active', 'Aktiv für Gäste')} value={event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')} /> } label={t('events.workspace.fields.date', 'Eventdatum')} value={formatDate(event.event_date)} /> } label={t('events.workspace.fields.eventType', 'Event-Typ')} value={resolveEventType(event)} /> {stats && (

{t('events.workspace.fields.insights', 'Letzte Aktivität')}

{t('events.workspace.fields.uploadsTotal', { defaultValue: '{{count}} Uploads gesamt', count: stats.uploads_total ?? stats.total ?? 0, })} {' · '} {t('events.workspace.fields.uploadsToday', { defaultValue: '{{count}} Uploads (24h)', count: stats.uploads_24h ?? stats.recent_uploads ?? 0, })}

{t('events.workspace.fields.likesTotal', { defaultValue: '{{count}} Likes vergeben', count: stats.likes_total ?? stats.likes ?? 0, })}

)}
); } function QuickActionsCard({ slug, busy, onToggle, navigate }: { slug: string; busy: boolean; onToggle: () => void | Promise; navigate: ReturnType }) { const { t } = useTranslation('management'); const gridItems = [ { key: 'photos', icon: , label: t('events.quickActions.moderate', 'Fotos moderieren'), description: t('events.quickActions.moderateDesc', 'Prüfe Uploads und veröffentliche Highlights.'), onClick: () => navigate(ADMIN_EVENT_PHOTOS_PATH(slug)), }, { key: 'tasks', icon: , label: t('events.quickActions.tasks', 'Aufgaben bearbeiten'), description: t('events.quickActions.tasksDesc', 'Passe Story-Prompts und Moderation an.'), onClick: () => navigate(ADMIN_EVENT_TASKS_PATH(slug)), }, { key: 'invites', icon: , label: t('events.quickActions.invites', 'Layouts & QR verwalten'), description: t('events.quickActions.invitesDesc', 'Aktualisiere QR-Kits und Einladungslayouts.'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=layout`), }, { key: 'roles', icon: , label: t('events.quickActions.roles', 'Team & Rollen anpassen'), description: t('events.quickActions.rolesDesc', 'Verwalte Moderatoren und Co-Leads.'), onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)), }, { key: 'print', icon: , label: t('events.quickActions.print', 'Layouts als PDF drucken'), description: t('events.quickActions.printDesc', 'Exportiere QR-Sets für den Druck.'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=export`), }, ]; return (
); } function MetricsGrid({ metrics, stats }: { metrics: EventToolkit['metrics'] | null | undefined; stats: EventStats | null }) { const { t } = useTranslation('management'); const cards = [ { icon: , label: t('events.metrics.uploadsTotal', 'Uploads gesamt'), value: metrics?.uploads_total ?? stats?.uploads_total ?? 0, }, { icon: , label: t('events.metrics.uploads24h', 'Uploads (24h)'), value: metrics?.uploads_24h ?? stats?.uploads_24h ?? 0, }, { icon: , label: t('events.metrics.pending', 'Fotos in Moderation'), value: metrics?.pending_photos ?? stats?.pending_photos ?? 0, }, { icon: , label: t('events.metrics.activeInvites', 'Aktive Einladungen'), value: metrics?.active_invites ?? 0, }, ]; return (
{cards.map((card) => (
{card.icon}

{card.label}

{card.value}

))}
); } function InviteSummary({ invites, navigateToInvites }: { invites: EventToolkit['invites'] | undefined; navigateToInvites: () => void }) { const { t } = useTranslation('management'); return (
{t('events.invites.activeCount', { defaultValue: '{{count}} aktiv', count: invites?.summary.active ?? 0 })} {t('events.invites.totalCount', { defaultValue: '{{count}} gesamt', count: invites?.summary.total ?? 0 })}
{invites?.items?.length ? (
    {invites.items.slice(0, 3).map((invite) => (
  • {invite.label ?? invite.url}

    {invite.url}

  • ))}
) : (

{t('events.invites.empty', 'Noch keine Einladungen erstellt.')}

)}
); } function TaskOverviewCard({ tasks, navigateToTasks }: { tasks: EventToolkit['tasks'] | undefined; navigateToTasks: () => void }) { const { t } = useTranslation('management'); return ( {t('events.tasks.summary', { defaultValue: '{{completed}} von {{total}} erledigt', completed: tasks?.summary.completed ?? 0, total: tasks?.summary.total ?? 0, })} )} />
{tasks?.items?.length ? (
{tasks.items.slice(0, 4).map((task) => ( ))}
) : (

{t('events.tasks.empty', 'Noch keine Aufgaben zugewiesen.')}

)}
); } function TaskRow({ task }: { task: EventToolkitTask }) { return (

{task.title}

{task.description ?

{task.description}

: null}
{task.is_completed ? 'Erledigt' : 'Offen'}
); } function PendingPhotosCard({ slug, photos, navigateToModeration, }: { slug: string; photos: TenantPhoto[]; navigateToModeration: () => void; }) { const { t } = useTranslation('management'); const [entries, setEntries] = React.useState(photos); const [updatingId, setUpdatingId] = React.useState(null); React.useEffect(() => { setEntries(photos); }, [photos]); const handleVisibility = async (photo: TenantPhoto, visible: boolean) => { setUpdatingId(photo.id); try { const updated = await updatePhotoVisibility(slug, photo.id, visible); setEntries((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success( visible ? t('events.photos.toastVisible', 'Foto wieder sichtbar gemacht.') : t('events.photos.toastHidden', 'Foto ausgeblendet.'), ); } catch (err) { toast.error( isAuthError(err) ? t('events.photos.errorAuth', 'Session abgelaufen. Bitte erneut anmelden.') : t('events.photos.errorVisibility', 'Sichtbarkeit konnte nicht geändert werden.'), ); } finally { setUpdatingId(null); } }; return ( {t('events.photos.pendingCount', { defaultValue: '{{count}} Fotos offen', count: entries.length })} )} />
{entries.length ? (
{entries.slice(0, 6).map((photo) => { const hidden = photo.status === 'hidden'; return (
{photo.caption
); })}
) : (

{t('events.photos.pendingEmpty', 'Aktuell warten keine Fotos auf Freigabe.')}

)}
); } function RecentUploadsCard({ slug, photos }: { slug: string; photos: TenantPhoto[] }) { const { t } = useTranslation('management'); const [entries, setEntries] = React.useState(photos); const [updatingId, setUpdatingId] = React.useState(null); React.useEffect(() => { setEntries(photos); }, [photos]); const handleVisibility = async (photo: TenantPhoto, visible: boolean) => { setUpdatingId(photo.id); try { const updated = await updatePhotoVisibility(slug, photo.id, visible); setEntries((prev) => prev.map((item) => (item.id === photo.id ? updated : item))); toast.success( visible ? t('events.photos.toastVisible', 'Foto wieder sichtbar gemacht.') : t('events.photos.toastHidden', 'Foto ausgeblendet.'), ); } catch (err) { toast.error( isAuthError(err) ? t('events.photos.errorAuth', 'Session abgelaufen. Bitte erneut anmelden.') : t('events.photos.errorVisibility', 'Sichtbarkeit konnte nicht geändert werden.'), ); } finally { setUpdatingId(null); } }; return (
{entries.length ? (
{entries.slice(0, 6).map((photo) => { const hidden = photo.status === 'hidden'; return (
{photo.caption
); })}
) : (

{t('events.photos.recentEmpty', 'Noch keine neuen Uploads.')}

)}
); } function FeedbackCard({ slug }: { slug: string }) { const { t } = useTranslation('management'); const [sentiment, setSentiment] = React.useState<'positive' | 'neutral' | 'negative' | null>(null); const [message, setMessage] = React.useState(''); const [busy, setBusy] = React.useState(false); const [submitted, setSubmitted] = React.useState(false); const [error, setError] = React.useState(null); const copy = { positive: t('events.feedback.positive', 'Super Lauf!'), neutral: t('events.feedback.neutral', 'Läuft'), negative: t('events.feedback.negative', 'Braucht Support'), }; return (
{error && ( {t('events.feedback.errorTitle', 'Feedback konnte nicht gesendet werden.')} {error} )}
{(Object.keys(copy) as Array<'positive' | 'neutral' | 'negative'>).map((key) => ( ))}