import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { CalendarDays, ChevronDown, MapPin } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Switch } from '@tamagui/switch'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton } from './components/Primitives'; import { MobileDateTimeInput, MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls'; import { LegalConsentSheet } from './components/LegalConsentSheet'; import { createEvent, getEvent, updateEvent, getEventTypes, getPackages, getTenantPackagesOverview, Package, TenantEvent, TenantEventType, TenantPackageSummary, trackOnboarding, } from '../api'; import { resolveEventSlugAfterUpdate } from './eventFormNavigation'; import { adminPath } from '../constants'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage, getApiValidationMessage, isApiError } from '../lib/apiError'; import toast from 'react-hot-toast'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; import { withAlpha } from './components/colors'; import { useAuth } from '../auth/context'; import { useEventContext } from '../context/EventContext'; type FormState = { name: string; date: string; eventTypeId: number | null; description: string; location: string; published: boolean; autoApproveUploads: boolean; tasksEnabled: boolean; packageId: number | null; servicePackageSlug: string | null; }; export default function MobileEventFormPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const slug = slugParam ?? null; const isEdit = Boolean(slug); const navigate = useNavigate(); const { t } = useTranslation(['management', 'common']); const { user } = useAuth(); const { selectEvent, refetch } = useEventContext(); const queryClient = useQueryClient(); const { text, muted, subtle, danger, border, surface, primary } = useAdminTheme(); const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin'; const [form, setForm] = React.useState({ name: '', date: '', eventTypeId: null, description: '', location: '', published: false, autoApproveUploads: true, tasksEnabled: true, packageId: null, servicePackageSlug: null, }); const [eventTypes, setEventTypes] = React.useState([]); const [typesLoading, setTypesLoading] = React.useState(false); const [packages, setPackages] = React.useState([]); const [packagesLoading, setPackagesLoading] = React.useState(false); const [kontingentOptions, setKontingentOptions] = React.useState>([]); const [kontingentLoading, setKontingentLoading] = React.useState(false); const [loading, setLoading] = React.useState(isEdit); const [saving, setSaving] = React.useState(false); const [consentOpen, setConsentOpen] = React.useState(false); const [consentBusy, setConsentBusy] = React.useState(false); const [pendingPayload, setPendingPayload] = React.useState[0] | null>(null); const [error, setError] = React.useState(null); const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events')); React.useEffect(() => { if (!slug) return; (async () => { setLoading(true); try { const data = await getEvent(slug); setForm({ name: renderName(data.name), date: toDateTimeLocal(data.event_date), eventTypeId: data.event_type_id ?? data.event_type?.id ?? null, description: typeof data.description === 'string' ? data.description : '', location: resolveLocation(data), published: data.status === 'published', autoApproveUploads: (data.settings?.guest_upload_visibility as string | undefined) === 'immediate', tasksEnabled: (data.settings?.engagement_mode as string | undefined) !== 'photo_only' && (data.engagement_mode as string | undefined) !== 'photo_only', packageId: null, servicePackageSlug: null, }); setError(null); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'Event konnte nicht geladen werden.'))); } } finally { setLoading(false); } })(); }, [slug, t, isEdit]); React.useEffect(() => { (async () => { setTypesLoading(true); try { const types = await getEventTypes(); setEventTypes(types); const preferredType = types.find((type) => type.slug === 'wedding') ?? types[0] ?? null; if (preferredType) { setForm((prev) => (prev.eventTypeId ? prev : { ...prev, eventTypeId: preferredType.id })); } } catch { // silently ignore; fallback to null } finally { setTypesLoading(false); } })(); }, []); React.useEffect(() => { if (!isSuperAdmin || isEdit) { return; } (async () => { setPackagesLoading(true); try { const data = await getPackages('endcustomer'); setPackages(data); setForm((prev) => { if (prev.packageId) { return prev; } const preferred = data.find((pkg) => pkg.id === 3) ?? data[0] ?? null; return { ...prev, packageId: preferred?.id ?? null }; }); } catch { setPackages([]); } finally { setPackagesLoading(false); } })(); }, [isSuperAdmin, isEdit]); React.useEffect(() => { if (isEdit) { return; } (async () => { setKontingentLoading(true); try { const overview = await getTenantPackagesOverview(); const packages = overview.packages ?? []; const active = packages.filter((pkg) => pkg.active && pkg.package_type === 'reseller'); const totals = new Map(); active.forEach((pkg: TenantPackageSummary) => { const slugValue = pkg.included_package_slug ?? 'standard'; if (!slugValue) { return; } const remaining = Number.isFinite(pkg.remaining_events as number) ? Number(pkg.remaining_events) : 0; if (remaining <= 0) { return; } totals.set(slugValue, (totals.get(slugValue) ?? 0) + remaining); }); const options = Array.from(totals.entries()) .map(([slugValue, remaining]) => ({ slug: slugValue, remaining })) .sort((a, b) => a.slug.localeCompare(b.slug)); setKontingentOptions(options); setForm((prev) => { if (prev.servicePackageSlug || options.length === 0) { return prev; } if (options.length === 1) { return { ...prev, servicePackageSlug: options[0].slug }; } const standard = options.find((row) => row.slug === 'standard'); return { ...prev, servicePackageSlug: standard?.slug ?? options[0].slug }; }); } catch { setKontingentOptions([]); } finally { setKontingentLoading(false); } })(); }, [isEdit]); const resolveServiceTierLabel = React.useCallback((slugValue: string) => { if (slugValue === 'starter') { return 'Starter'; } if (slugValue === 'standard') { return 'Standard'; } if (slugValue === 'pro') { return 'Premium'; } return slugValue; }, []); async function handleSubmit() { setSaving(true); setError(null); try { if (isEdit && slug) { const updated = await updateEvent(slug, { name: form.name, event_date: form.date || undefined, event_type_id: form.eventTypeId ?? undefined, status: form.published ? 'published' : 'draft', settings: { location: form.location, guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review', engagement_mode: form.tasksEnabled ? 'tasks' : 'photo_only', }, }); const nextSlug = resolveEventSlugAfterUpdate(slug, updated); await queryClient.invalidateQueries({ queryKey: ['tenant-events'] }); void refetch(); navigate(adminPath(`/mobile/events/${nextSlug}`)); } else { const payload = { name: form.name || t('eventForm.fields.name.fallback', 'Event'), slug: `${Date.now()}`, event_type_id: form.eventTypeId ?? undefined, event_date: form.date || undefined, status: form.published ? 'published' : 'draft', package_id: isSuperAdmin ? form.packageId ?? undefined : undefined, service_package_slug: form.servicePackageSlug ?? undefined, settings: { location: form.location, guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review', engagement_mode: form.tasksEnabled ? 'tasks' : 'photo_only', }, } as Parameters[0]; const { event } = await createEvent(payload); if (typeof window !== 'undefined' && form.tasksEnabled) { try { window.sessionStorage.setItem(`tasksDecisionPrompt:${event.id}`, 'pending'); } catch { // ignore storage exceptions } } selectEvent(event.slug); void queryClient.invalidateQueries({ queryKey: ['tenant-events'] }); void refetch(); void trackOnboarding('event_created', { event_id: event.id }); navigate(adminPath(`/mobile/events/${event.slug}`)); } } catch (err) { if (isAuthError(err)) { return; } if (!isEdit && isWaiverRequiredError(err)) { const payload = { name: form.name || t('eventForm.fields.name.fallback', 'Event'), slug: `${Date.now()}`, event_type_id: form.eventTypeId ?? undefined, event_date: form.date || undefined, status: form.published ? 'published' : 'draft', package_id: isSuperAdmin ? form.packageId ?? undefined : undefined, service_package_slug: form.servicePackageSlug ?? undefined, settings: { location: form.location, guest_upload_visibility: form.autoApproveUploads ? 'immediate' : 'review', engagement_mode: form.tasksEnabled ? 'tasks' : 'photo_only', }, } as Parameters[0]; setPendingPayload(payload); setConsentOpen(true); return; } const message = getApiValidationMessage(err, t('eventForm.errors.saveFailed', 'Event could not be saved.')); setError(message); toast.error(message); } finally { setSaving(false); } } async function handleConsentConfirm(consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) { if (!pendingPayload) { setConsentOpen(false); return; } setConsentBusy(true); try { const { event } = await createEvent({ ...pendingPayload, accepted_waiver: consents.acceptedWaiver, }); if (typeof window !== 'undefined' && form.tasksEnabled) { try { window.sessionStorage.setItem(`tasksDecisionPrompt:${event.id}`, 'pending'); } catch { // ignore storage exceptions } } selectEvent(event.slug); void queryClient.invalidateQueries({ queryKey: ['tenant-events'] }); void refetch(); void trackOnboarding('event_created', { event_id: event.id }); navigate(adminPath(`/mobile/events/${event.slug}`)); setConsentOpen(false); setPendingPayload(null); } catch (err) { if (!isAuthError(err)) { const message = getApiValidationMessage(err, t('eventForm.errors.saveFailed', 'Event could not be saved.')); setError(message); toast.error(message); } } finally { setConsentBusy(false); } } return ( {error ? ( {error} ) : null} setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder={t('eventForm.fields.name.placeholder', 'e.g. Summer Party 2025')} /> {isSuperAdmin && !isEdit ? ( {packagesLoading ? ( {t('eventForm.fields.package.loading', 'Loading packages…')} ) : packages.length === 0 ? ( {t('eventForm.fields.package.empty', 'No packages available yet.')} ) : ( setForm((prev) => ({ ...prev, packageId: Number(e.target.value) }))} > {packages.map((pkg) => ( ))} )} {t('eventForm.fields.package.help', 'This controls the event’s premium limits.')} ) : null} {!isEdit && (kontingentLoading || kontingentOptions.length > 0) ? ( {kontingentLoading ? ( {t('eventForm.fields.servicePackage.loading', 'Loading Event-Kontingente…')} ) : ( setForm((prev) => ({ ...prev, servicePackageSlug: String(e.target.value) }))} > {kontingentOptions.map((opt) => ( ))} )} {t( 'eventForm.fields.servicePackage.help', 'Wählt das Event-Level. Pro Event wird 1 aus dem passenden Event-Kontingent verbraucht.', )} ) : null} setForm((prev) => ({ ...prev, date: event.target.value }))} style={{ flex: 1 }} /> {typesLoading ? ( {t('eventForm.fields.type.loading', 'Loading event types…')} ) : eventTypes.length === 0 ? ( {t('eventForm.fields.type.empty', 'No event types available yet. Please add one in the admin area.')} ) : ( setForm((prev) => ({ ...prev, eventTypeId: Number(e.target.value) }))} > {eventTypes.map((type) => ( ))} )} setForm((prev) => ({ ...prev, description: e.target.value }))} placeholder={t('eventForm.fields.description.placeholder', 'Description')} /> setForm((prev) => ({ ...prev, location: e.target.value }))} placeholder={t('eventForm.fields.location.placeholder', 'Location')} style={{ flex: 1 }} /> setForm((prev) => ({ ...prev, published: Boolean(checked) })) } size="$3" aria-label={t('eventForm.fields.publish.label', 'Publish immediately')} > {form.published ? t('common:states.enabled', 'Enabled') : t('common:states.disabled', 'Disabled')} {t('eventForm.fields.publish.help', 'Enable if guests should see the event right away. You can change the status later.')} setForm((prev) => ({ ...prev, tasksEnabled: Boolean(checked) })) } size="$3" aria-label={t('eventForm.fields.tasksMode.label', 'Tasks & challenges')} > {form.tasksEnabled ? t('common:states.enabled', 'Enabled') : t('common:states.disabled', 'Disabled')} {form.tasksEnabled ? t( 'eventForm.fields.tasksMode.helpOn', 'Guests can see tasks, challenges and achievements.', ) : t( 'eventForm.fields.tasksMode.helpOff', 'Task mode is off: guests only see the photo feed.', )} setForm((prev) => ({ ...prev, autoApproveUploads: Boolean(checked) })) } size="$3" aria-label={t('eventForm.fields.uploadVisibility.label', 'Uploads visible immediately')} > {form.autoApproveUploads ? t('common:states.enabled', 'Enabled') : t('common:states.disabled', 'Disabled')} {form.autoApproveUploads ? t( 'eventForm.fields.uploadVisibility.helpOn', 'Neue Gast-Uploads erscheinen sofort in der Galerie (Security-Scan läuft im Hintergrund).', ) : t( 'eventForm.fields.uploadVisibility.helpOff', 'Uploads werden zunächst geprüft und erscheinen nach Freigabe.', )} {!isEdit ? ( ) : null} handleSubmit()} /> { if (consentBusy) return; setConsentOpen(false); setPendingPayload(null); }} onConfirm={handleConsentConfirm} busy={consentBusy} requireTerms={false} requireWaiver copy={{ title: t('events.eventStartConsent.title', 'Before your first event'), description: t( 'events.eventStartConsent.description', 'Please confirm the immediate start of the digital service before creating your first event.', ), checkboxWaiver: t( 'events.eventStartConsent.checkboxWaiver', 'I expressly request that the digital service begins now and understand my right of withdrawal expires once the contract has been fully performed.', ), errorWaiver: t( 'events.eventStartConsent.errorWaiver', 'Please confirm the immediate start of the digital service and the early expiry of the right of withdrawal.', ), confirm: t('events.eventStartConsent.confirm', 'Create event'), cancel: t('events.eventStartConsent.cancel', 'Cancel'), }} t={t} /> ); } function renderName(name: TenantEvent['name']): string { if (typeof name === 'string') return name; if (name && typeof name === 'object') { return name.de ?? name.en ?? Object.values(name)[0] ?? ''; } return ''; } function isWaiverRequiredError(error: unknown): boolean { if (!isApiError(error)) { return false; } const metaErrors = error.meta?.errors; if (!metaErrors || typeof metaErrors !== 'object') { return false; } return 'accepted_waiver' in metaErrors; } function toDateTimeLocal(value?: string | null): string { if (!value) return ''; const parsed = new Date(value); if (!Number.isNaN(parsed.getTime())) { return parsed.toISOString().slice(0, 16); } const fallback = value.replace(' ', 'T'); return fallback.length >= 16 ? fallback.slice(0, 16) : ''; } function resolveLocation(event: TenantEvent): string { const settings = (event.settings ?? {}) as Record; const candidate = (settings.location as string | undefined) ?? (settings.address as string | undefined) ?? (settings.city as string | undefined); if (candidate && candidate.trim()) return candidate; return ''; }