// @ts-nocheck import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ArrowLeft, Bell, Camera, CheckCircle2, Circle, Clock3, Loader2, MessageSquare, Printer, QrCode, PlugZap, Smile, Sparkles, ShoppingCart, Menu, Users, AlertTriangle, } 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, TenantEmotion, TenantEvent, TenantPhoto, EventStats, getEvent, getEventStats, getEventToolkit, toggleEvent, submitTenantFeedback, updatePhotoVisibility, createEventAddonCheckout, featurePhoto, unfeaturePhoto, getEmotions, } 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_PHOTOBOOTH_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENT_TASKS_PATH, ADMIN_EVENT_BRANDING_PATH, buildEngagementTabPath, } from '../constants'; import { buildEventTabs } from '../lib/eventTabs'; import { formatEventDate } from '../lib/events'; import { SectionCard, SectionHeader, } from '../components/tenant'; import { AddonsPicker } from '../components/Addons/AddonsPicker'; import { AddonSummaryList } from '../components/Addons/AddonSummaryList'; import { EventAddonCatalogItem, getAddonCatalog } from '../api'; import { GuestBroadcastCard } from '../components/GuestBroadcastCard'; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { filterEmotionsByEventType } from '../lib/emotions'; 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() { const { slug: slugParam } = useParams<{ slug?: string }>(); const [searchParams] = useSearchParams(); 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 [addonBusyId, setAddonBusyId] = React.useState(null); const [addonRefreshCount, setAddonRefreshCount] = React.useState(0); const [addonsCatalog, setAddonsCatalog] = React.useState([]); const [emotions, setEmotions] = React.useState([]); 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, addonOptions] = await Promise.all([getEvent(slug), getEventStats(slug), getAddonCatalog()]); setState((prev) => ({ ...prev, event: eventData, stats: statsData, loading: false })); setAddonsCatalog(addonOptions); } 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]); React.useEffect(() => { let cancelled = false; (async () => { try { const list = await getEmotions(); if (!cancelled) { setEmotions(list); } } catch (error) { if (!isAuthError(error)) { console.warn('Failed to load emotions for event detail', error); } } })(); return () => { cancelled = true; }; }, []); 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 = t('events.workspace.detailSubtitle', 'Behalte Status, Aufgaben und QR-Codes deines Events im Blick.'); const currentTabKey = 'overview'; const eventTabs = React.useMemo(() => { if (!event) return []; const translateMenu = (key: string, fallback: string) => t(key, { defaultValue: fallback }); const counts = { photos: stats?.uploads_total ?? event.photo_count ?? undefined, tasks: toolkitData?.tasks?.summary?.total ?? event.tasks_count ?? undefined, }; return buildEventTabs(event, translateMenu, counts); }, [event, stats?.uploads_total, toolkitData?.tasks?.summary?.total, t]); const brandingAllowed = React.useMemo(() => { const settings = (event?.settings ?? {}) as Record; return Boolean(settings.branding_allowed ?? true); }, [event]); const limitWarnings = React.useMemo( () => (event?.limits ? buildLimitWarnings(event.limits, (key, options) => tCommon(`limits.${key}`, options)) : []), [event?.limits, tCommon], ); const [dismissedWarnings, setDismissedWarnings] = React.useState>(new Set()); React.useEffect(() => { const slug = event?.slug; if (!slug || typeof window === 'undefined') { setDismissedWarnings(new Set()); return; } try { const raw = window.localStorage.getItem(`tenant-admin:dismissed-limit-warnings:${slug}`); if (!raw) { setDismissedWarnings(new Set()); return; } const parsed = JSON.parse(raw) as string[]; setDismissedWarnings(new Set(parsed)); } catch { setDismissedWarnings(new Set()); } }, [event?.slug]); const visibleWarnings = React.useMemo( () => limitWarnings.filter((warning) => !dismissedWarnings.has(warning.id)), [limitWarnings, dismissedWarnings], ); const dismissWarning = React.useCallback( (id: string) => { const slug = event?.slug; setDismissedWarnings((prev) => { const next = new Set(prev); next.add(id); if (slug && typeof window !== 'undefined') { window.localStorage.setItem( `tenant-admin:dismissed-limit-warnings:${slug}`, JSON.stringify(Array.from(next)), ); } return next; }); }, [event?.slug], ); const shownWarningToasts = React.useRef>(new Set()); //const [addonBusyId, setAddonBusyId] = React.useState(null); const handleAddonPurchase = React.useCallback( async (scope: 'photos' | 'guests' | 'gallery', addonKeyOverride?: string) => { if (!slug) return; const defaultAddons: Record = { photos: 'extra_photos_500', guests: 'extra_guests_100', gallery: 'extend_gallery_30d', }; const addonKey = addonKeyOverride ?? defaultAddons[scope]; setAddonBusyId(scope); try { const currentUrl = window.location.origin + window.location.pathname; const successUrl = `${currentUrl}?addon_success=1`; const checkout = await createEventAddonCheckout(slug, { addon_key: addonKey, quantity: 1, success_url: successUrl, cancel_url: currentUrl, }); if (checkout.checkout_url) { window.location.href = checkout.checkout_url; } else { toast(t('events.errors.checkoutMissing', 'Checkout konnte nicht gestartet werden.')); } } catch (err) { toast(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Add-on Checkout fehlgeschlagen.'))); } finally { setAddonBusyId(null); } }, [slug, t], ); 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]); React.useEffect(() => { const success = searchParams.get('addon_success'); if (success && slug) { toast(t('events.success.addonApplied', 'Add-on angewendet. Limits aktualisieren sich in Kürze.')); void load(); setAddonRefreshCount(3); const params = new URLSearchParams(window.location.search); params.delete('addon_success'); const search = params.toString(); navigate(search ? `${window.location.pathname}?${search}` : window.location.pathname, { replace: true }); } }, [searchParams, slug, load, navigate, t]); React.useEffect(() => { if (addonRefreshCount <= 0) { return; } const timer = setTimeout(() => { void load(); setAddonRefreshCount((count) => count - 1); }, 8000); return () => clearTimeout(timer); }, [addonRefreshCount, load]); 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} )} {visibleWarnings.length > 0 && (
{visibleWarnings.map((warning) => (
{warning.message}
{(['photos', 'guests', 'gallery'] as const).includes(warning.scope) ? ( <> {addonsCatalog.length > 0 ? ( { void handleAddonPurchase(warning.scope as 'photos' | 'guests' | 'gallery', key); }} busy={addonBusyId === warning.scope} t={(key, fallback) => t(key, fallback)} /> ) : null} ) : null}
))}
)} {toolkit.error && ( {toolkit.error} )} {loading ? ( ) : event ? (

{t('events.workspace.hero.badge', 'Event')}

{resolveName(event.name)}

{t('events.workspace.hero.description', 'Konzentriere dich auf Aufgaben, Moderation und QR-Code fĂĽr dieses Event.')}

{getStatusLabel(event, t)} {formatDate(event.event_date)} {t('events.workspace.hero.liveBadge', 'Live?')} {event.is_active ? t('events.workspace.activeYes', 'Ja') : t('events.workspace.activeNo', 'Nein')} {resolveEventType(event)}
navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} /> navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} />
{brandingAllowed ? ( navigate(ADMIN_EVENT_BRANDING_PATH(event.slug))} onOpenCollections={() => navigate(buildEngagementTabPath('collections'))} onOpenTasks={() => navigate(ADMIN_EVENT_TASKS_PATH(event.slug))} onOpenEmotions={() => navigate(buildEngagementTabPath('emotions'))} /> ) : null} {event.addons?.length ? ( t(key, fallback)} /> ) : null} navigate(`${ADMIN_EVENT_INVITES_PATH(event.slug)}?tab=layout`)} /> {event.limits?.gallery ? : null}
) : (

{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'; } type RecapContentProps = { event: TenantEvent; stats: EventStats; busy: boolean; onToggleEvent: () => void; onExtendGallery: () => void; }; function RecapContent({ event, stats, busy, onToggleEvent, onExtendGallery }: RecapContentProps) { const { t } = useTranslation('management'); const navigate = useNavigate(); const galleryExpiresAt = event.package?.expires_at ?? event.limits?.gallery?.expires_at ?? null; const galleryStatusLabel = event.is_active ? t('events.recap.galleryOpen', 'Galerie geöffnet') : t('events.recap.galleryClosed', 'Galerie geschlossen'); const counts = { photos: stats.uploads_total ?? stats.total ?? 0, pending: stats.pending_photos ?? 0, likes: stats.likes_total ?? stats.likes ?? 0, }; return (

{t('events.recap.galleryTitle', 'Galerie-Status')}

{galleryStatusLabel}

{t('events.recap.galleryCounts', '{{photos}} Fotos, {{pending}} offen, {{likes}} Likes', counts)}

{event.is_active ? t('events.recap.open', 'Offen') : t('events.recap.closed', 'Geschlossen')}
{event.public_url ? (
{event.public_url}
) : null}

{t('events.recap.exportTitle', 'Export & Backup')}

{t('events.recap.exportCopy', 'Alle Assets sichern')}

{t('events.recap.exportHint', 'Zip/CSV Export und Backup anstoĂźen.')}

{t('events.recap.backup', 'Backup')}

{t('events.recap.retentionTitle', 'Aufbewahrung & Verlängerung')}

{galleryExpiresAt ? t('events.recap.expiresAt', 'Läuft ab am {{date}}', { date: formatEventDate(galleryExpiresAt, undefined) }) : t('events.recap.noExpiry', 'Ablaufdatum nicht gesetzt')}

{t('events.recap.retentionHint', 'Verlängere die Galerie-Laufzeit mit einem Add-on. Verlängerungen addieren sich.')}

{t('events.recap.expiry', 'Ablauf')}

{t('events.recap.commsTitle', 'Kommunikation')}

{t('events.recap.commsCopy', 'Kein Gast-Newsletter aktiv')}

{t('events.recap.commsHint', 'Push wirkt vor allem live. Teile den Link manuell mit deinem Team oder auf Social Media.')}

{t('events.recap.manual', 'Manuell')}
{event.public_url ? (
{event.public_url}
) : null}
); } function QuickActionsMenu({ slug, navigate }: { slug: string; navigate: ReturnType }) { const { t } = useTranslation('management'); const actions = [ { key: 'photos', icon: , label: t('events.quickActions.moderate', 'Fotos moderieren'), onClick: () => navigate(ADMIN_EVENT_PHOTOS_PATH(slug)) }, { key: 'tasks', icon: , label: t('events.quickActions.tasks', 'Aufgaben bearbeiten'), onClick: () => navigate(ADMIN_EVENT_TASKS_PATH(slug)) }, { key: 'invites', icon: , label: t('events.quickActions.invites', 'Layouts & QR verwalten'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=layout`) }, { key: 'roles', icon: , label: t('events.quickActions.roles', 'Team & Rollen anpassen'), onClick: () => navigate(ADMIN_EVENT_MEMBERS_PATH(slug)) }, { key: 'photobooth', icon: , label: t('events.quickActions.photobooth', 'Photobooth anbinden'), onClick: () => navigate(ADMIN_EVENT_PHOTOBOOTH_PATH(slug)) }, { key: 'print', icon: , label: t('events.quickActions.print', 'Layouts als PDF drucken'), onClick: () => navigate(`${ADMIN_EVENT_INVITES_PATH(slug)}?tab=export`) }, ]; return ( {t('events.quickActions.title', 'Starte in die Moderation')}
{actions.map((action) => ( ))}
); } 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 QR-Codes 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 }) { const { t } = useTranslation('management'); return (

{task.title}

{task.description ?

{task.description}

: null}
{task.is_completed ? t('events.tasks.status.completed', 'Done') : t('events.tasks.status.open', 'Open')}
); } function BrandingMissionCard({ event, invites, emotions, onOpenBranding, onOpenCollections, onOpenTasks, onOpenEmotions, }: { event: TenantEvent; invites?: EventToolkit['invites']; emotions?: TenantEmotion[]; onOpenBranding: () => void; onOpenCollections: () => void; onOpenTasks: () => void; onOpenEmotions: () => void; }) { const { t } = useTranslation('management'); const palette = extractBrandingPalette(event.settings); const activeInvites = invites?.summary.active ?? 0; const eventTypeId = event.event_type_id ?? event.event_type?.id ?? null; const spotlightEmotions = React.useMemo( () => filterEmotionsByEventType(emotions ?? [], eventTypeId).slice(0, 4), [emotions, eventTypeId], ); return (

{t('events.branding.brandingTitle', 'Branding')}

{palette.font ?? t('events.branding.brandingFallback', 'Aktuelle Auswahl')}

{t('events.branding.brandingCopy', 'Passe Farben & Schriftarten im Layout-Editor an.')}

{(palette.colors.length ? palette.colors : ['#f472b6', '#fef3c7', '#312e81']).map((color) => ( ))}

{t('events.branding.collectionsTitle', 'Aufgaben-Sets')}

{event.event_type?.name ?? t('events.branding.collectionsFallback', 'Empfohlene Story')}

{t('events.branding.collectionsCopy', 'Importiere passende Kollektionen oder aktiviere Emotionen im Aufgabenbereich.')}

{t('events.branding.collectionsActive', { defaultValue: '{{count}} aktive Links', count: activeInvites })} {t('events.branding.tasksCount', { defaultValue: '{{count}} Aufgaben', count: Number(event.tasks_count ?? 0), })}

{t('events.branding.emotionsTitle', 'Emotionen')}

{spotlightEmotions.length ? (
{spotlightEmotions.map((emotion) => ( {emotion.icon ? {emotion.icon} : null} {emotion.name} ))}
) : (

{t('events.branding.emotionsEmpty', 'Aktiviere Emotionen, um Aufgaben zu kategorisieren.')}

)}
); } function GalleryShareCard({ invites, onManageInvites, }: { invites?: EventToolkit['invites']; onManageInvites: () => void; }) { const { t } = useTranslation('management'); const primaryInvite = React.useMemo( () => invites?.items?.find((invite) => invite.is_active) ?? invites?.items?.[0] ?? null, [invites?.items], ); const handleCopy = React.useCallback(async () => { if (!primaryInvite?.url) { return; } try { await navigator.clipboard.writeText(primaryInvite.url); toast.success(t('events.galleryShare.copied', 'Link kopiert')); } catch (err) { console.error(err); toast.error(t('events.galleryShare.copyFailed', 'Konnte Link nicht kopieren')); } }, [primaryInvite, t]); if (!primaryInvite) { return ( ); } return (

{primaryInvite.label ?? t('events.galleryShare.linkLabel', 'Standard-Link')}

{primaryInvite.url}

{t('events.galleryShare.scans', { defaultValue: '{{count}} Aufrufe', count: primaryInvite.usage_count })} {typeof primaryInvite.usage_limit === 'number' && ( {t('events.galleryShare.limit', { defaultValue: 'Limit {{count}}', count: primaryInvite.usage_limit })} )}
); } function GalleryStatusCard({ gallery }: { gallery: GallerySummary }) { const { t } = useTranslation('management'); const stateLabel = gallery.state === 'expired' ? t('events.galleryStatus.stateExpired', 'Galerie abgelaufen') : gallery.state === 'warning' ? t('events.galleryStatus.stateWarning', 'Galerie läuft bald ab') : t('events.galleryStatus.stateOk', 'Galerie aktiv'); const expiresLabel = gallery.expires_at && gallery.state !== 'unlimited' ? formatDate(gallery.expires_at) : t('events.galleryStatus.noExpiry', 'Kein Ablaufdatum gesetzt'); const daysRemaining = typeof gallery.days_remaining === 'number' && gallery.days_remaining >= 0 ? gallery.days_remaining : null; return (

{t('events.galleryStatus.stateLabel', 'Status')}

{stateLabel}

{t('events.galleryStatus.expiresAt', { defaultValue: 'Ablaufdatum: {{date}}', date: expiresLabel, })}

{t('events.galleryStatus.daysLabel', 'Verbleibende Tage')}

{daysRemaining !== null ? daysRemaining : '—'}

); } function extractBrandingPalette( settings: TenantEvent['settings'], ): { colors: string[]; font?: string } { const colors: string[] = []; let font: string | undefined; if (settings && typeof settings === 'object') { const brandingSource = (settings as Record).branding && typeof (settings as Record).branding === 'object' ? (settings as Record).branding : settings; const candidateKeys = ['primary_color', 'secondary_color', 'accent_color', 'background_color', 'color']; candidateKeys.forEach((key) => { const value = (brandingSource as Record)[key]; if (typeof value === 'string' && value.trim()) { colors.push(value); } }); const fontKeys = ['font_family', 'font', 'heading_font']; fontKeys.some((key) => { const value = (brandingSource as Record)[key]; if (typeof value === 'string' && value.trim()) { font = value; return true; } return false; }); } return { colors, font }; } // Pending photos summary moved to the dedicated Live/Photos view. 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))); } 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 {photo.is_featured ? ( Highlight ) : null}
♥ {photo.likes_count} {photo.uploader_name ?? 'Gast'}
); })}
) : (

{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(undefined); 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) => ( ))}