// @ts-nocheck import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ArrowLeft, Camera, Clock3, Loader2, MessageSquare, Printer, ShoppingCart, Sparkles } 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 { Switch } from '@/components/ui/switch'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { AdminLayout } from '../components/AdminLayout'; import { createEventAddonCheckout, EventQrInvite, EventStats, getEvent, getEventStats, getEventQrInvites, getAddonCatalog, type EventAddonCatalogItem, TenantEvent, toggleEvent, submitTenantFeedback, } from '../api'; import { updateEvent } from '../api'; import { buildEventTabs } from '../lib/eventTabs'; import { formatEventDate } from '../lib/events'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { ADMIN_EVENT_EDIT_PATH, ADMIN_EVENT_PHOTOS_PATH, ADMIN_EVENTS_PATH, } from '../constants'; export default function EventRecapPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const navigate = useNavigate(); const { t } = useTranslation('management'); const [searchParams, setSearchParams] = useSearchParams(); const slug = slugParam ?? null; const [event, setEvent] = React.useState(null); const [stats, setStats] = React.useState(null); const [loading, setLoading] = React.useState(true); const [busy, setBusy] = React.useState(false); const [settingsBusy, setSettingsBusy] = React.useState(false); const [error, setError] = React.useState(null); const [joinTokens, setJoinTokens] = React.useState([]); const [addonsCatalog, setAddonsCatalog] = React.useState([]); const [addonBusyKey, setAddonBusyKey] = React.useState(null); const loadEventData = React.useCallback(async () => { if (!slug) { setLoading(false); setError(t('events.errors.missingSlug', 'Kein Event ausgewählt.')); return; } setLoading(true); setError(null); try { const [eventData, statsData, invites, addons] = await Promise.all([ getEvent(slug), getEventStats(slug), getEventQrInvites(slug), getAddonCatalog(), ]); setEvent(eventData); setStats(statsData); setJoinTokens(invites); setAddonsCatalog(addons); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.'))); } } finally { setLoading(false); } }, [slug, t]); React.useEffect(() => { void loadEventData(); }, [loadEventData]); React.useEffect(() => { if (!searchParams.get('addon_success')) { return; } toast(t('events.success.addonApplied', 'Add-on angewendet. Limits aktualisieren sich in Kürze.')); void loadEventData(); const next = new URLSearchParams(searchParams); next.delete('addon_success'); setSearchParams(next, { replace: true }); }, [loadEventData, searchParams, setSearchParams, t]); const handleToggleEvent = React.useCallback(async () => { if (!slug) return; setBusy(true); setError(null); try { const updated = await toggleEvent(slug); setEvent(updated); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.toggleFailed', 'Status konnte nicht angepasst werden.'))); } } finally { setBusy(false); } }, [slug, t]); const handleAddonCheckout = React.useCallback(async (addonKey: string) => { if (!slug) return; setAddonBusyKey(addonKey); setError(null); 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) { if (!isAuthError(err)) { toast(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Add-on Checkout fehlgeschlagen.'))); } } finally { setAddonBusyKey(null); } }, [slug, t]); const handleArchive = React.useCallback(async () => { if (!slug || !event) return; setArchiveBusy(true); setError(null); try { const updated = await updateEvent(slug, { status: 'archived', is_active: false }); setEvent(updated); setArchiveOpen(false); toast.success(t('events.recap.archivedSuccess', 'Event archiviert. Galerie ist geschlossen.')); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.archiveFailed', 'Archivierung fehlgeschlagen.'))); } } finally { setArchiveBusy(false); } }, [event, slug, t]); const handleToggleSetting = React.useCallback(async (key: 'guest_downloads_enabled' | 'guest_sharing_enabled', value: boolean) => { if (!slug || !event) return; setSettingsBusy(true); setError(null); try { const updated = await updateEvent(slug, { settings: { ...(event.settings ?? {}), [key]: value, }, }); setEvent(updated); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.toggleFailed', 'Einstellung konnte nicht gespeichert werden.'))); } } finally { setSettingsBusy(false); } }, [event, slug, t]); if (!slug) { return ( {t('events.errors.notFoundTitle', 'Event nicht gefunden')} {t('events.errors.notFoundBody', 'Kehre zur Eventliste zurück und wähle dort ein Event aus.')} ); } const eventName = event ? resolveName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event'); const activeInvite = joinTokens.find((token) => token.is_active); const guestLinkRaw = event?.public_url ?? activeInvite?.url ?? (activeInvite?.token ? `/g/${activeInvite.token}` : null); const guestLink = buildAbsoluteGuestLink(guestLinkRaw); const guestQrCodeDataUrl = activeInvite?.qr_code_data_url ?? null; const eventTabs = event ? buildEventTabs(event, (key, fallback) => t(key, { defaultValue: fallback }), { photos: stats?.uploads_total ?? event.photo_count ?? undefined, tasks: event.tasks_count ?? undefined, }) : []; return ( {error && ( {t('events.alerts.failedTitle', 'Aktion fehlgeschlagen')} {error} )} {loading ? ( ) : event && stats ? ( { navigator.clipboard.writeText(guestLink); toast.success(t('events.recap.copySuccess', 'Link kopiert')); } : undefined} onOpenPhotos={() => navigate(ADMIN_EVENT_PHOTOS_PATH(event.slug))} onEditEvent={() => navigate(ADMIN_EVENT_EDIT_PATH(event.slug))} onBack={() => navigate(ADMIN_EVENTS_PATH)} settingsBusy={settingsBusy} onToggleDownloads={(value) => handleToggleSetting('guest_downloads_enabled', value)} onToggleSharing={(value) => handleToggleSetting('guest_sharing_enabled', value)} /> ) : ( {t('events.errors.notFoundTitle', 'Event nicht gefunden')} {t('events.errors.notFoundBody', 'Kehre zur Eventliste zurück und wähle dort ein Event aus.')} )} ); } type RecapContentProps = { event: TenantEvent; stats: EventStats; busy: boolean; onToggleEvent: () => void; guestLink: string | null; guestQrCodeDataUrl: string | null; addonsCatalog: EventAddonCatalogItem[]; addonBusyKey: string | null; onCheckoutAddon: (addonKey: string) => void; onArchive: () => void; onCopyLink?: () => void; onOpenPhotos: () => void; onEditEvent: () => void; onBack: () => void; settingsBusy: boolean; onToggleDownloads: (value: boolean) => void; onToggleSharing: (value: boolean) => void; }; function RecapContent({ event, stats, busy, onToggleEvent, guestLink, guestQrCodeDataUrl, addonsCatalog, addonBusyKey, onCheckoutAddon, onArchive, onCopyLink, onOpenPhotos, onEditEvent, onBack, settingsBusy, onToggleDownloads, onToggleSharing, }: RecapContentProps) { const { t } = useTranslation('management'); 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, }; const [sentiment, setSentiment] = React.useState<'positive' | 'neutral' | 'negative' | null>(null); const [feedbackMessage, setFeedbackMessage] = React.useState(''); const [feedbackBusy, setFeedbackBusy] = React.useState(false); const [feedbackSubmitted, setFeedbackSubmitted] = React.useState(false); const [feedbackError, setFeedbackError] = React.useState(undefined); const [archiveOpen, setArchiveOpen] = React.useState(false); const [archiveBusy, setArchiveBusy] = React.useState(false); const [archiveConfirmed, setArchiveConfirmed] = React.useState(false); const [feedbackOpen, setFeedbackOpen] = React.useState(false); const [feedbackBestArea, setFeedbackBestArea] = React.useState(null); const [feedbackNeedsSupport, setFeedbackNeedsSupport] = React.useState(false); const galleryAddons = React.useMemo( () => addonsCatalog.filter((addon) => addon.key.includes('gallery') || addon.key.includes('boost')), [addonsCatalog], ); const addonsToShow = galleryAddons.length ? galleryAddons : addonsCatalog; const defaultAddon = addonsToShow[0] ?? null; const describeAddon = React.useCallback((addon: EventAddonCatalogItem): string | null => { const increments = addon.increments ?? {}; const photos = (increments as Record).photos ?? (increments as Record).extra_photos; const guests = (increments as Record).guests ?? (increments as Record).extra_guests; const galleryDays = (increments as Record).gallery_days ?? (increments as Record).extra_gallery_days; const parts: string[] = []; if (typeof photos === 'number' && photos > 0) { parts.push(t('events.sections.addons.summary.photos', `+${photos} Fotos`, { count: photos.toLocaleString() })); } if (typeof guests === 'number' && guests > 0) { parts.push(t('events.sections.addons.summary.guests', `+${guests} Gäste`, { count: guests.toLocaleString() })); } if (typeof galleryDays === 'number' && galleryDays > 0) { parts.push(t('events.sections.addons.summary.gallery', `+${galleryDays} Tage`, { count: galleryDays })); } return parts.length ? parts.join(' · ') : null; }, [t]); const copy = { positive: t('events.feedback.positive', 'War super'), neutral: t('events.feedback.neutral', 'In Ordnung'), negative: t('events.feedback.negative', 'Brauch(t)e Unterstützung'), }; const bestAreaOptions = [ { key: 'uploads', label: t('events.feedback.best.uploads', 'Uploads & Geschwindigkeit') }, { key: 'invites', label: t('events.feedback.best.invites', 'QR-Einladungen & Layouts') }, { key: 'moderation', label: t('events.feedback.best.moderation', 'Moderation & Export') }, { key: 'experience', label: t('events.feedback.best.experience', 'Allgemeine App-Erfahrung') }, ]; const handleQrDownload = React.useCallback(() => { if (!guestQrCodeDataUrl) return; const link = document.createElement('a'); link.href = guestQrCodeDataUrl; link.download = 'guest-gallery-qr.png'; link.click(); }, [guestQrCodeDataUrl]); const handleQrShare = React.useCallback(async () => { if (!guestLink) return; if (navigator.share) { try { await navigator.share({ title: resolveName(event.name), text: t('events.recap.shareGuests', 'Gäste-Galerie teilen'), url: guestLink, }); return; } catch { // Ignore share cancellation and fall back to copy. } } try { await navigator.clipboard.writeText(guestLink); toast.success(t('events.recap.copySuccess', 'Link kopiert')); } catch { toast.error(t('events.recap.copyError', 'Link konnte nicht geteilt werden.')); } }, [event.name, guestLink, t]); const handleFeedbackSubmit = React.useCallback(async () => { if (feedbackBusy) return; setFeedbackBusy(true); setFeedbackError(undefined); try { await submitTenantFeedback({ category: 'event_workspace_after_event', event_slug: event.slug, sentiment: sentiment ?? undefined, message: feedbackMessage.trim() ? feedbackMessage.trim() : undefined, metadata: { best_area: feedbackBestArea, needs_support: feedbackNeedsSupport, event_name: resolveName(event.name), guest_link: guestLink, }, }); setFeedbackSubmitted(true); setFeedbackOpen(false); setFeedbackMessage(''); setFeedbackNeedsSupport(false); toast.success(t('events.feedback.submitted', 'Danke!')); } catch (err) { setFeedbackError(isAuthError(err) ? t('events.feedback.authError', 'Deine Session ist abgelaufen. Bitte melde dich erneut an.') : t('events.feedback.genericError', 'Feedback konnte nicht gesendet werden.')); } finally { setFeedbackBusy(false); } }, [event.slug, event.name, feedbackBestArea, feedbackBusy, feedbackMessage, feedbackNeedsSupport, feedbackSubmitted, guestLink, sentiment, t]); return (

{t('events.recap.badge', 'Nachbereitung')}

{resolveName(event.name)}

{t('events.recap.subtitle', 'Abschluss, Export und Galerie-Laufzeit verwalten.')}

{event.status === 'published' ? t('events.status.published', 'Veröffentlicht') : t('events.status.draft', 'Entwurf')} {event.event_date ? formatEventDate(event.event_date, undefined) : t('events.workspace.noDate', 'Kein Datum')}

{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')}

{t('events.recap.shareLink', 'Gäste-Link')}

{guestLink ? ( {guestLink} ) : (

{t('events.recap.noPublicUrl', 'Kein Gäste-Link gesetzt. Lege den öffentlichen Link im Event-Setup fest.')}

)}
{guestLink && onCopyLink ? ( ) : null}

{t('events.recap.allowDownloads', 'Downloads erlauben')}

{t('events.recap.allowDownloadsHint', 'Gäste dürfen Fotos speichern')}

onToggleDownloads(Boolean(checked))} disabled={settingsBusy} />

{t('events.recap.allowSharing', 'Teilen erlauben')}

{t('events.recap.allowSharingHint', 'Gäste dürfen Links teilen')}

onToggleSharing(Boolean(checked))} disabled={settingsBusy} />
{guestQrCodeDataUrl ? (
{t('events.recap.qrAlt',

{t('events.recap.qrTitle', 'QR-Code teilen')}

{guestLink ? (

{guestLink}

) : null}
) : 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.highlightsHint', '“Highlights” = als Highlight markierte Fotos in der Galerie.')}

{t('events.recap.retentionTitle', 'Verlängerung / Archivierung')}

{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')}
{addonsToShow.length ? (

{t('events.recap.extendOptions', 'Alle Add-ons für dieses Event')}

{addonsToShow.map((addon) => (

{addon.label}

{describeAddon(addon) ?? t('events.recap.extendHint', 'Laufzeitverlängerungen addieren sich. Checkout öffnet in einem neuen Tab.')}

))}

{t('events.recap.extendHint', 'Laufzeitverlängerungen addieren sich. Checkout öffnet in einem neuen Tab.')}

) : (

{t('events.recap.noAddons', 'Aktuell keine Add-ons verfügbar.')}

)}

{t('events.feedback.badge', 'Feedback')}

{t('events.feedback.afterEventTitle', 'Event beendet – kurzes Feedback?')}

{t('events.feedback.afterEventCopy', 'Hat alles geklappt? Deine Antwort hilft uns für kommende Events.')}
{t('events.feedback.privacyHint', 'Nur Admin-Feedback, keine Gastdaten')}

{t('events.feedback.badgeShort', 'Feedback')}
{resolveName(event.name)} {event.event_date ? ( {formatEventDate(event.event_date, undefined)} ) : null}
{feedbackSubmitted ? (

{t('events.feedback.submitted', 'Danke!')}

{t('events.feedback.afterEventThanks', 'Dein Feedback ist angekommen. Wir melden uns, falls Rückfragen bestehen.')}

) : (
{t('events.feedback.quickSentiment', 'Stimmung auswählbar (positiv/neutral/Support).')}
)} {feedbackError ? ( {t('events.feedback.errorTitle', 'Feedback konnte nicht gesendet werden.')} {feedbackError} ) : null}
{ setFeedbackOpen(open); setFeedbackError(undefined); }}> {t('events.feedback.dialogTitle', 'Kurzes After-Event Feedback')} {t('events.feedback.dialogCopy', 'Wähle eine Stimmung, was am besten lief und optional, was wir verbessern sollen.')}

{t('events.feedback.sentiment', 'Stimmung')}

{(Object.keys(copy) as Array<'positive' | 'neutral' | 'negative'>).map((key) => ( ))}

{t('events.feedback.bestQuestion', 'Was lief am besten?')}

{bestAreaOptions.map((option) => ( ))}

{t('events.feedback.improve', 'Was sollen wir verbessern?')}