import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ChevronRight, RefreshCcw, ArrowLeft } from 'lucide-react'; import { YStack, XStack, Stack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge } from './components/Primitives'; import { TenantEvent, EventQrInvite, EventQrInviteLayout, getEvent, getEventQrInvites, createQrInvite, updateEventQrInvite, } from '../api'; import { isAuthError } from '../auth/tokens'; import { getApiErrorMessage } from '../lib/apiError'; import toast from 'react-hot-toast'; export default function MobileQrPrintPage() { const { slug: slugParam } = useParams<{ slug?: string }>(); const slug = slugParam ?? null; const navigate = useNavigate(); const { t } = useTranslation('management'); const [event, setEvent] = React.useState(null); const [selectedInvite, setSelectedInvite] = React.useState(null); const [selectedLayoutId, setSelectedLayoutId] = React.useState(null); const [error, setError] = React.useState(null); const [loading, setLoading] = React.useState(true); const [qrUrl, setQrUrl] = React.useState(''); const [wizardStep, setWizardStep] = React.useState<'select-layout' | 'background' | 'text' | 'preview'>('select-layout'); const [selectedBackgroundPreset, setSelectedBackgroundPreset] = React.useState(null); const [textFields, setTextFields] = React.useState({ headline: '', subtitle: '', description: '', instructions: [''], }); const [saving, setSaving] = React.useState(false); const BACKGROUND_PRESETS = [ { id: 'bg-blue-floral', src: '/storage/layouts/backgrounds-portrait/bg-blue-floral.png', label: 'Blue Floral' }, { id: 'bg-goldframe', src: '/storage/layouts/backgrounds-portrait/bg-goldframe.png', label: 'Gold Frame' }, { id: 'gr-green-floral', src: '/storage/layouts/backgrounds-portrait/gr-green-floral.png', label: 'Green Floral' }, ]; React.useEffect(() => { if (!slug) return; (async () => { setLoading(true); try { const data = await getEvent(slug); const invites = await getEventQrInvites(slug); setEvent(data); const primaryInvite = invites.find((item) => item.is_active) ?? invites[0] ?? null; setSelectedInvite(primaryInvite); setSelectedLayoutId(primaryInvite?.layouts?.[0]?.id ?? null); const backgroundPreset = (primaryInvite?.metadata as any)?.layout_customization?.background_preset ?? null; setSelectedBackgroundPreset(typeof backgroundPreset === 'string' ? backgroundPreset : null); const customization = (primaryInvite?.metadata as any)?.layout_customization ?? {}; setTextFields({ headline: customization.headline ?? '', subtitle: customization.subtitle ?? '', description: customization.description ?? '', instructions: Array.isArray(customization.instructions) && customization.instructions.length ? customization.instructions.map((item: unknown) => String(item ?? '')).filter((item: string) => item.length > 0) : [''], }); setQrUrl(primaryInvite?.url ?? data.public_url ?? ''); setError(null); } catch (err) { if (!isAuthError(err)) { setError(getApiErrorMessage(err, t('events.errors.loadFailed', 'QR-Daten konnten nicht geladen werden.'))); } } finally { setLoading(false); } })(); }, [slug, t]); return ( navigate(-1)} headerActions={ window.location.reload()}> } > {error ? ( {error} ) : null} {t('events.qr.heroTitle', 'Entrance QR Code')} {qrUrl ? ( QR ) : ( {t('events.qr.missing', 'Kein QR-Link vorhanden')} )} {t('events.qr.description', 'Scan to access the event guest app.')} { if (qrUrl) { toast.success(t('events.qr.downloadStarted', 'Download gestartet')); } else { toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden')); } }} /> { try { const shareUrl = String(qrUrl || (event as any)?.public_url || ''); await navigator.clipboard.writeText(shareUrl); toast.success(t('events.qr.shareSuccess', 'Link kopiert')); } catch { toast.error(t('events.qr.shareFailed', 'Konnte Link nicht kopieren')); } }} /> {t('events.qr.layouts', 'Print Layouts')} {(() => { if (wizardStep === 'select-layout') { return ( { setSelectedLayoutId(layoutId); setWizardStep('background'); }} /> ); } if (wizardStep === 'background') { return ( setWizardStep('select-layout')} presets={BACKGROUND_PRESETS} selectedPreset={selectedBackgroundPreset} onSelectPreset={setSelectedBackgroundPreset} selectedLayout={selectedInvite?.layouts.find((l) => l.id === selectedLayoutId) ?? null} onSave={async () => { if (!slug || !selectedInvite || !selectedLayoutId) { toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden')); return; } setSaving(true); try { const payload = { metadata: { layout_customization: { layout_id: selectedLayoutId, background_preset: selectedBackgroundPreset, headline: textFields.headline || undefined, subtitle: textFields.subtitle || undefined, description: textFields.description || undefined, instructions: textFields.instructions.filter((item) => item.trim().length > 0), }, }, }; const updated = await updateEventQrInvite(slug, selectedInvite.id, payload); setSelectedInvite(updated); toast.success(t('common.saved', 'Gespeichert')); setWizardStep('text'); } catch (err) { toast.error(getApiErrorMessage(err, t('events.errors.saveFailed', 'Speichern fehlgeschlagen.'))); } finally { setSaving(false); } }} saving={saving} /> ); } if (wizardStep === 'text') { return ( setWizardStep('background')} textFields={textFields} onChange={(fields) => setTextFields(fields)} onSave={async () => { if (!slug || !selectedInvite || !selectedLayoutId) { toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden')); return; } setSaving(true); try { const payload = { metadata: { layout_customization: { layout_id: selectedLayoutId, background_preset: selectedBackgroundPreset, headline: textFields.headline || null, subtitle: textFields.subtitle || null, description: textFields.description || null, instructions: textFields.instructions.filter((item) => item.trim().length > 0), }, }, }; const updated = await updateEventQrInvite(slug, selectedInvite.id, payload); setSelectedInvite(updated); toast.success(t('common.saved', 'Gespeichert')); setWizardStep('preview'); } catch (err) { toast.error(getApiErrorMessage(err, t('events.errors.saveFailed', 'Speichern fehlgeschlagen.'))); } finally { setSaving(false); } }} saving={saving} /> ); } return ( setWizardStep('text')} layout={selectedInvite?.layouts.find((l) => l.id === selectedLayoutId) ?? null} backgroundPreset={selectedBackgroundPreset} presets={BACKGROUND_PRESETS} textFields={textFields} qrUrl={qrUrl} onExport={(format) => { const layout = selectedInvite?.layouts.find((l) => l.id === selectedLayoutId); const url = layout?.download_urls?.[format]; if (!url) { toast.error(t('events.qr.missing', 'Kein QR-Link vorhanden')); return; } window.open(url, '_blank', 'noopener'); }} /> ); })()} {t('events.qr.templates', 'Templates')} {t('events.qr.branding', 'Branding')} {t('events.qr.paper', 'Paper Size')} {t('events.qr.paperAuto', 'Auto (per layout)')} toast.success(t('events.qr.previewStarted', 'Preview gestartet (mock)'))} /> { if (!slug) return; try { if (!slug) return; const invite = await createQrInvite(slug, { label: 'Mobile Link' }); setQrUrl(invite.url); toast.success(t('events.qr.created', 'Neuer QR-Link erstellt')); } catch (err) { toast.error(getApiErrorMessage(err, t('events.qr.createFailed', 'Link konnte nicht erstellt werden.'))); } }} /> ); } function LayoutSelection({ layouts, selectedLayoutId, onSelect, }: { layouts: EventQrInviteLayout[]; selectedLayoutId: string | null; onSelect: (layoutId: string) => void; }) { const { t } = useTranslation('management'); if (!layouts.length) { return ( {t('events.qr.noLayouts', 'Keine Layouts verfügbar.')} ); } return ( {layouts.map((layout) => { const isSelected = layout.id === selectedLayoutId; return ( onSelect(layout.id)} style={{ width: '100%' }}> {layout.name || layout.id} {layout.description ? ( {layout.description} ) : null} {(layout.paper || 'A4').toUpperCase()} {(layout.orientation || 'portrait').toUpperCase()} {layout.panel_mode ? {layout.panel_mode} : null} ); })} ); } function BackgroundStep({ onBack, presets, selectedPreset, onSelectPreset, selectedLayout, onSave, saving, }: { onBack: () => void; presets: { id: string; src: string; label: string }[]; selectedPreset: string | null; onSelectPreset: (id: string) => void; selectedLayout: EventQrInviteLayout | null; onSave: () => void; saving: boolean; }) { const { t } = useTranslation('management'); return ( {t('common.back', 'Zurück')} {selectedLayout ? `${(selectedLayout.paper || 'A4').toUpperCase()} • ${(selectedLayout.orientation || 'portrait').toUpperCase()}` : 'Layout'} {t('events.qr.backgroundPicker', 'Hintergrund auswählen (A4 Portrait Presets)')} {presets.map((preset) => { const isSelected = selectedPreset === preset.id; return ( onSelectPreset(preset.id)} style={{ width: '48%' }}> {preset.label} {isSelected ? {t('common.selected', 'Ausgewählt')} : null} ); })} {t('events.qr.backgroundNote', 'Diese Presets sind für A4 Hochformat. Spiegelung erfolgt automatisch bei Tischkarten.')} ); } function TextStep({ onBack, textFields, onChange, onSave, saving, }: { onBack: () => void; textFields: { headline: string; subtitle: string; description: string; instructions: string[] }; onChange: (fields: { headline: string; subtitle: string; description: string; instructions: string[] }) => void; onSave: () => void; saving: boolean; }) { const { t } = useTranslation('management'); const updateField = (key: 'headline' | 'subtitle' | 'description', value: string) => { onChange({ ...textFields, [key]: value }); }; const updateInstruction = (idx: number, value: string) => { const next = [...textFields.instructions]; next[idx] = value; onChange({ ...textFields, instructions: next }); }; const addInstruction = () => { onChange({ ...textFields, instructions: [...textFields.instructions, ''] }); }; const removeInstruction = (idx: number) => { const next = textFields.instructions.filter((_, i) => i !== idx); onChange({ ...textFields, instructions: next.length ? next : [''] }); }; return ( {t('common.back', 'Zurück')} {t('events.qr.textStep', 'Texte & Hinweise')} {t('events.qr.textFields', 'Texte')} updateField('headline', val)} /> updateField('subtitle', val)} /> updateField('description', val)} /> {t('events.qr.instructions', 'Anleitung')} {textFields.instructions.map((item, idx) => ( updateInstruction(idx, val)} /> removeInstruction(idx)} disabled={textFields.instructions.length === 1} /> ))} ); } function PreviewStep({ onBack, layout, backgroundPreset, presets, textFields, qrUrl, onExport, }: { onBack: () => void; layout: EventQrInviteLayout | null; backgroundPreset: string | null; presets: { id: string; src: string; label: string }[]; textFields: { headline: string; subtitle: string; description: string; instructions: string[] }; qrUrl: string; onExport: (format: 'pdf' | 'png') => void; }) { const { t } = useTranslation('management'); const presetSrc = backgroundPreset ? presets.find((p) => p.id === backgroundPreset)?.src ?? null : null; const resolvedBg = presetSrc ?? layout?.preview?.background ?? '#f8fafc'; return ( {t('common.back', 'Zurück')} {layout ? ( {(layout.paper || 'A4').toUpperCase()} • {(layout.orientation || 'portrait').toUpperCase()} ) : null} {t('events.qr.preview', 'Vorschau')} {textFields.headline || layout?.name || t('events.qr.previewHeadline', 'Event QR')} {textFields.subtitle ? ( {textFields.subtitle} ) : null} {textFields.description ? ( {textFields.description} ) : null} {textFields.instructions.filter((i) => i.trim().length > 0).map((item, idx) => ( • {item} ))} {qrUrl ? ( QR ) : ( {t('events.qr.missing', 'Kein QR-Link vorhanden')} )} onExport('pdf')} /> onExport('png')} /> ); } function StyledInput({ value, onChangeText, placeholder, flex, }: { value: string; onChangeText: (value: string) => void; placeholder?: string; flex?: number; }) { return ( onChangeText(e.target.value)} placeholder={placeholder} /> ); } function StyledTextarea({ value, onChangeText, placeholder, }: { value: string; onChangeText: (value: string) => void; placeholder?: string; }) { return (