import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; 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 { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls'; import { LegalConsentSheet } from './components/LegalConsentSheet'; import { createEvent, getEvent, updateEvent, getEventTypes, TenantEvent, TenantEventType, 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'; type FormState = { name: string; date: string; eventTypeId: number | null; description: string; location: string; published: boolean; autoApproveUploads: boolean; tasksEnabled: boolean; }; 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 { text, muted, subtle, danger, border, surface, primary } = useAdminTheme(); const [form, setForm] = React.useState({ name: '', date: '', eventTypeId: null, description: '', location: '', published: false, autoApproveUploads: true, tasksEnabled: true, }); const [eventTypes, setEventTypes] = React.useState([]); const [typesLoading, setTypesLoading] = 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', }); 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); } })(); }, []); 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); 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', 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); 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', 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, }); 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')} /> setForm((prev) => ({ ...prev, date: value }))} border={border} surface={surface} text={text} primary={primary} danger={danger} 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 NativeDateTimeInput({ value, onChange, border, surface, text, primary, danger, hasError, style, }: { value: string; onChange: (value: string) => void; border: string; surface: string; text: string; primary: string; danger: string; hasError?: boolean; style?: React.CSSProperties; }) { const [focused, setFocused] = React.useState(false); const ringColor = hasError ? withAlpha(danger, 0.18) : withAlpha(primary, 0.18); const borderColor = hasError ? danger : focused ? primary : border; return ( onChange(event.target.value)} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} style={{ width: '100%', height: 44, padding: '0 12px', borderRadius: 12, borderWidth: 1, borderStyle: 'solid', borderColor, backgroundColor: surface, color: text, fontSize: 14, boxShadow: focused ? `0 0 0 3px ${ringColor}` : undefined, outline: 'none', ...style, }} /> ); } 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 ''; }