import React from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { Sparkles, Camera, Link2, QrCode, RefreshCcw, Shield, Archive, ShoppingCart, Clock3, Share2 } from 'lucide-react'; import toast from 'react-hot-toast'; import { getEvent, getEventStats, getEventQrInvites, toggleEvent, updateEvent, createEventAddonCheckout, getAddonCatalog, submitTenantFeedback, type TenantEvent, type EventStats, type EventQrInvite, type EventAddonCatalogItem, } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import { HeaderActionButton, MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { adminPath } from '../constants'; import { selectAddonKeyForScope } from './addons'; import { LegalConsentSheet } from './components/LegalConsentSheet'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; export default function MobileEventRecapPage() { const { slug } = useParams<{ slug?: string }>(); const navigate = useNavigate(); const location = useLocation(); const { t } = useTranslation('management'); const { textStrong, text, muted, subtle, danger, border, primary, accentSoft } = useAdminTheme(); const [event, setEvent] = React.useState(null); const [stats, setStats] = React.useState(null); const [invites, setInvites] = React.useState([]); const [addons, setAddons] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [busy, setBusy] = React.useState(false); const [archiveBusy, setArchiveBusy] = React.useState(false); const [checkoutBusy, setCheckoutBusy] = React.useState(false); const [consentOpen, setConsentOpen] = React.useState(false); const [consentBusy, setConsentBusy] = React.useState(false); const [consentAddonKey, setConsentAddonKey] = React.useState(null); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); const load = React.useCallback(async () => { if (!slug) return; setLoading(true); try { const [eventData, statsData, inviteData, addonData] = await Promise.all([ getEvent(slug), getEventStats(slug), getEventQrInvites(slug), getAddonCatalog(), ]); setEvent(eventData); setStats(statsData); setInvites(inviteData ?? []); setAddons(addonData ?? []); setError(null); } 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 load(); }, [load]); React.useEffect(() => { if (!location.search) return; const params = new URLSearchParams(location.search); if (params.get('addon_success')) { toast.success(t('events.success.addonApplied', 'Add-on angewendet. Limits aktualisieren sich in Kürze.')); params.delete('addon_success'); navigate({ pathname: location.pathname, search: params.toString() ? `?${params.toString()}` : '' }, { replace: true }); void load(); } }, [location.search, location.pathname, t, navigate, load]); if (!slug) { return ( {t('events.errors.notFoundBody', 'Kehre zur Eventliste zurück und wähle dort ein Event aus.')} ); } const activeInvite = invites.find((invite) => invite.is_active); const guestLink = event?.public_url ?? activeInvite?.url ?? (activeInvite?.token ? `/g/${activeInvite.token}` : null); const galleryCounts = { photos: stats?.uploads_total ?? stats?.total ?? 0, pending: stats?.pending_photos ?? 0, likes: stats?.likes_total ?? stats?.likes ?? 0, }; async function toggleGallery() { if (!slug) return; setBusy(true); 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); } } async function archiveEvent() { if (!slug || !event) return; setArchiveBusy(true); try { const updated = await updateEvent(slug, { status: 'archived', is_active: false }); setEvent(updated); 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); } } function startAddonCheckout() { if (!slug) return; const addonKey = selectAddonKeyForScope(addons, 'gallery'); setConsentAddonKey(addonKey); setConsentOpen(true); } async function confirmAddonCheckout(consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) { if (!slug || !consentAddonKey) return; const currentUrl = typeof window !== 'undefined' ? `${window.location.origin}${adminPath(`/mobile/events/${slug}/recap`)}` : ''; const successUrl = `${currentUrl}?addon_success=1`; setCheckoutBusy(true); setConsentBusy(true); try { const checkout = await createEventAddonCheckout(slug, { addon_key: consentAddonKey, quantity: 1, success_url: successUrl, cancel_url: currentUrl, accepted_terms: consents.acceptedTerms, accepted_waiver: consents.acceptedWaiver, }); if (checkout.checkout_url) { window.location.href = checkout.checkout_url; } else { toast.error(t('events.errors.checkoutMissing', 'Checkout konnte nicht gestartet werden.')); } } catch (err) { if (!isAuthError(err)) { toast.error(getApiErrorMessage(err, t('events.errors.checkoutFailed', 'Add-on Checkout fehlgeschlagen.'))); } } finally { setCheckoutBusy(false); setConsentBusy(false); setConsentOpen(false); setConsentAddonKey(null); } } async function submitFeedback(sentiment: 'positive' | 'neutral' | 'negative') { if (!event) return; try { await submitTenantFeedback({ category: 'event_workspace_after_event', event_slug: event.slug, sentiment, metadata: { event_name: resolveName(event.name), guest_link: guestLink, }, }); toast.success(t('events.feedback.submitted', 'Danke!')); } catch (err) { toast.error(getApiErrorMessage(err, t('events.feedback.genericError', 'Feedback konnte nicht gesendet werden.'))); } } return ( load()} ariaLabel={t('common.refresh', 'Refresh')}> } > {error ? ( {error} ) : null} {loading ? ( {Array.from({ length: 4 }).map((_, idx) => ( ))} ) : event && stats ? ( {t('events.recap.badge', 'Nachbereitung')} {resolveName(event.name)} {t('events.recap.subtitle', 'Abschluss, Export und Galerie-Laufzeit verwalten.')} {event.is_active ? t('events.recap.galleryOpen', 'Galerie geöffnet') : t('events.recap.galleryClosed', 'Galerie geschlossen')} navigate(adminPath(`/mobile/events/${slug}/photos`))} /> navigate(adminPath(`/mobile/events/${slug}/edit`))} /> {t('events.recap.galleryTitle', 'Galerie-Status')} {t('events.recap.galleryCounts', '{{photos}} Fotos, {{pending}} offen, {{likes}} Likes', galleryCounts)} {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 && copyToClipboard(guestLink, t)} /> {guestLink ? ( shareLink(guestLink, event, t)} /> ) : null} {activeInvite?.qr_code_data_url ? ( {t('events.qr.qrAlt', downloadQr(activeInvite.qr_code_data_url)} /> ) : null} {t('events.sections.addons.title', 'Add-ons & Upgrades')} {t('events.sections.addons.description', 'Zusätzliche Kontingente freischalten.')} { startAddonCheckout(); }} loading={checkoutBusy} /> {t('events.recap.settingsTitle', 'Gast-Einstellungen')} updateSetting(event, setEvent, slug, 'guest_downloads_enabled', value, setError, t)} /> updateSetting(event, setEvent, slug, 'guest_sharing_enabled', value, setError, t)} /> {t('events.recap.archiveTitle', 'Event archivieren')} {t('events.recap.archiveCopy', 'Schließt die Galerie und markiert das Event als abgeschlossen.')} archiveEvent()} loading={archiveBusy} /> {t('events.recap.feedbackTitle', 'Wie lief das Event?')} void submitFeedback('positive')} /> void submitFeedback('neutral')} /> void submitFeedback('negative')} /> ) : null} { if (consentBusy) return; setConsentOpen(false); setConsentAddonKey(null); }} onConfirm={confirmAddonCheckout} busy={consentBusy} t={t} /> ); } function Stat({ label, value }: { label: string; value: string }) { const { border, muted, textStrong } = useAdminTheme(); return ( {label} {value} ); } function ToggleRow({ label, value, onToggle }: { label: string; value: boolean; onToggle: (value: boolean) => void }) { const { textStrong } = useAdminTheme(); return ( {label} onToggle(e.target.checked)} style={{ width: 20, height: 20 }} /> ); } async function updateSetting( event: TenantEvent, setEvent: (event: TenantEvent) => void, slug: string | undefined, key: 'guest_downloads_enabled' | 'guest_sharing_enabled', value: boolean, setError: (message: string | null) => void, t: (key: string, fallback?: string) => string, ): Promise { if (!slug) return; 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.'))); } } } function copyToClipboard(value: string, t: (key: string, fallback?: string) => string) { navigator.clipboard .writeText(value) .then(() => toast.success(t('events.recap.copySuccess', 'Link kopiert'))) .catch(() => toast.error(t('events.recap.copyError', 'Link konnte nicht kopiert werden.'))); } async function shareLink(value: string, event: TenantEvent, t: (key: string, fallback?: string) => string) { if (navigator.share) { try { await navigator.share({ title: resolveName(event.name), text: t('events.recap.shareGuests', 'Gäste-Galerie teilen'), url: value, }); return; } catch { // ignore } } copyToClipboard(value, t); } function downloadQr(dataUrl: string) { const link = document.createElement('a'); link.href = dataUrl; link.download = 'guest-gallery-qr.png'; link.click(); } function resolveName(name: TenantEvent['name']): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? 'Event'; } return 'Event'; } function formatDate(iso?: string | null): string { if (!iso) return ''; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ''; return date.toLocaleDateString(undefined, { day: '2-digit', month: 'short', year: 'numeric' }); }