import React from 'react'; import { useTranslation } from 'react-i18next'; import { CheckCircle2, Lock, MailWarning, User } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Pressable } from '@tamagui/react-native-web-lite'; import toast from 'react-hot-toast'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge } from './components/Primitives'; import { MobileField, MobileInput, MobileSelect, MobileColorInput } from './components/FormControls'; import { fetchTenantProfile, updateTenantProfile, getTenantPackagesOverview, getTenantSettings, updateTenantSettings, type TenantAccountProfile, } from '../api'; import { getApiErrorMessage, getApiValidationMessage } from '../lib/apiError'; import { ADMIN_PROFILE_PATH } from '../constants'; import { useBackNavigation } from './hooks/useBackNavigation'; import { useAdminTheme } from './theme'; import i18n from '../i18n'; import { extractBrandingForm, type BrandingFormValues } from '../lib/brandingForm'; import { DEFAULT_EVENT_BRANDING } from '@/guest/context/EventBrandingContext'; type ProfileFormState = { name: string; email: string; preferredLocale: string; currentPassword: string; password: string; passwordConfirmation: string; }; const LOCALE_OPTIONS = [ { value: '', labelKey: 'profile.locale.auto', fallback: 'Automatisch' }, { value: 'de', label: 'Deutsch' }, { value: 'en', label: 'English' }, ]; type TabKey = 'account' | 'branding'; const TENANT_BRANDING_DEFAULTS = { primary: DEFAULT_EVENT_BRANDING.primaryColor, accent: DEFAULT_EVENT_BRANDING.secondaryColor, background: DEFAULT_EVENT_BRANDING.backgroundColor, surface: DEFAULT_EVENT_BRANDING.palette?.surface ?? DEFAULT_EVENT_BRANDING.backgroundColor, headingFont: DEFAULT_EVENT_BRANDING.typography?.heading ?? DEFAULT_EVENT_BRANDING.fontFamily ?? '', bodyFont: DEFAULT_EVENT_BRANDING.typography?.body ?? DEFAULT_EVENT_BRANDING.fontFamily ?? '', mode: DEFAULT_EVENT_BRANDING.mode ?? 'auto', buttonStyle: DEFAULT_EVENT_BRANDING.buttons?.style ?? 'filled', buttonRadius: DEFAULT_EVENT_BRANDING.buttons?.radius ?? 12, buttonPrimary: DEFAULT_EVENT_BRANDING.buttons?.primary ?? DEFAULT_EVENT_BRANDING.primaryColor, buttonSecondary: DEFAULT_EVENT_BRANDING.buttons?.secondary ?? DEFAULT_EVENT_BRANDING.secondaryColor, linkColor: DEFAULT_EVENT_BRANDING.buttons?.linkColor ?? DEFAULT_EVENT_BRANDING.secondaryColor, fontSize: DEFAULT_EVENT_BRANDING.typography?.sizePreset ?? 'm', logoMode: DEFAULT_EVENT_BRANDING.logo?.mode ?? 'emoticon', logoPosition: DEFAULT_EVENT_BRANDING.logo?.position ?? 'left', logoSize: DEFAULT_EVENT_BRANDING.logo?.size ?? 'm', }; const buildTenantBrandingFormBase = (): BrandingFormValues => ({ ...TENANT_BRANDING_DEFAULTS, logoDataUrl: '', logoValue: '', }); export default function MobileProfileAccountPage() { const { t } = useTranslation('settings'); const { text, muted, danger, subtle, primary, accentSoft } = useAdminTheme(); const back = useBackNavigation(ADMIN_PROFILE_PATH); const [profile, setProfile] = React.useState(null); const [activeTab, setActiveTab] = React.useState('account'); const [brandingAllowed, setBrandingAllowed] = React.useState(false); const [brandingLoading, setBrandingLoading] = React.useState(true); const [brandingSaving, setBrandingSaving] = React.useState(false); const [brandingError, setBrandingError] = React.useState(null); const [tenantSettings, setTenantSettings] = React.useState | null>(null); const [brandingForm, setBrandingForm] = React.useState(buildTenantBrandingFormBase); const [form, setForm] = React.useState({ name: '', email: '', preferredLocale: '', currentPassword: '', password: '', passwordConfirmation: '', }); const [loading, setLoading] = React.useState(true); const [savingAccount, setSavingAccount] = React.useState(false); const [savingPassword, setSavingPassword] = React.useState(false); const [error, setError] = React.useState(null); const loadErrorMessage = t('profile.errors.load', 'Profil konnte nicht geladen werden.'); const brandingLoadError = t('profile.errors.brandingLoad', 'Standard-Branding konnte nicht geladen werden.'); const brandingSaveError = t('profile.errors.brandingSave', 'Standard-Branding konnte nicht gespeichert werden.'); const dateFormatter = React.useMemo( () => new Intl.DateTimeFormat(i18n.language || 'de', { day: '2-digit', month: 'long', year: 'numeric', }), [i18n.language], ); React.useEffect(() => { (async () => { setLoading(true); try { const data = await fetchTenantProfile(); setProfile(data); setForm((prev) => ({ ...prev, name: data.name ?? '', email: data.email ?? '', preferredLocale: data.preferred_locale ?? '', })); setError(null); } catch (err) { setError(getApiErrorMessage(err, loadErrorMessage)); } finally { setLoading(false); } })(); }, []); React.useEffect(() => { let active = true; (async () => { try { const overview = await getTenantPackagesOverview(); if (!active) return; setBrandingAllowed(Boolean(overview.activePackage?.branding_allowed)); } catch { if (active) { setBrandingAllowed(false); } } })(); return () => { active = false; }; }, []); React.useEffect(() => { let active = true; (async () => { setBrandingLoading(true); try { const payload = await getTenantSettings(); if (!active) return; const settings = (payload.settings ?? {}) as Record; setTenantSettings(settings); setBrandingForm(extractBrandingForm(settings, TENANT_BRANDING_DEFAULTS)); setBrandingError(null); } catch (err) { if (active) { setBrandingError(getApiErrorMessage(err, brandingLoadError)); } } finally { if (active) { setBrandingLoading(false); } } })(); return () => { active = false; }; }, [brandingLoadError]); const verifiedAt = profile?.email_verified_at ? new Date(profile.email_verified_at) : null; const verifiedDate = verifiedAt ? dateFormatter.format(verifiedAt) : null; const emailStatusLabel = profile?.email_verified ? t('profile.status.emailVerified', 'E-Mail bestätigt') : t('profile.status.emailNotVerified', 'Bestätigung erforderlich'); const emailHint = profile?.email_verified ? t('profile.status.verifiedHint', 'Bestätigt am {{date}}.', { date: verifiedDate ?? '' }) : t('profile.status.unverifiedHint', 'Bei Änderung der E-Mail senden wir dir automatisch eine neue Bestätigung.'); const brandingTabEnabled = brandingAllowed; React.useEffect(() => { if (!brandingTabEnabled && activeTab === 'branding') { setActiveTab('account'); } }, [brandingTabEnabled, activeTab]); const buildPayload = (includePassword: boolean) => ({ name: form.name.trim(), email: form.email.trim(), preferred_locale: form.preferredLocale ? form.preferredLocale : null, ...(includePassword ? { current_password: form.currentPassword, password: form.password, password_confirmation: form.passwordConfirmation, } : {}), }); const handleAccountSave = async () => { setSavingAccount(true); try { const updated = await updateTenantProfile(buildPayload(false)); setProfile(updated); setError(null); toast.success(t('profile.toasts.updated', 'Profil wurde aktualisiert.')); } catch (err) { const message = getApiValidationMessage(err, t('profile.errors.update', 'Profil konnte nicht aktualisiert werden.')); setError(message); toast.error(message); } finally { setSavingAccount(false); } }; const handlePasswordSave = async () => { setSavingPassword(true); try { const updated = await updateTenantProfile(buildPayload(true)); setProfile(updated); setError(null); setForm((prev) => ({ ...prev, currentPassword: '', password: '', passwordConfirmation: '', })); toast.success(t('profile.toasts.passwordChanged', 'Passwort wurde aktualisiert.')); } catch (err) { const message = getApiValidationMessage(err, t('profile.errors.update', 'Profil konnte nicht aktualisiert werden.')); setError(message); toast.error(message); } finally { setSavingPassword(false); } }; const handleBrandingSave = async () => { if (!tenantSettings) { return; } setBrandingSaving(true); try { const existingBranding = tenantSettings && typeof (tenantSettings as Record).branding === 'object' ? ((tenantSettings as Record).branding as Record) : {}; const settings = { ...tenantSettings, branding: { ...existingBranding, primary_color: brandingForm.primary, secondary_color: brandingForm.accent, accent_color: brandingForm.accent, background_color: brandingForm.background, surface_color: brandingForm.surface, font_family: brandingForm.bodyFont, heading_font: brandingForm.headingFont, body_font: brandingForm.bodyFont, font_size: brandingForm.fontSize, mode: brandingForm.mode, typography: { ...(typeof existingBranding.typography === 'object' ? (existingBranding.typography as Record) : {}), heading: brandingForm.headingFont, body: brandingForm.bodyFont, size: brandingForm.fontSize, }, palette: { ...(typeof existingBranding.palette === 'object' ? (existingBranding.palette as Record) : {}), primary: brandingForm.primary, secondary: brandingForm.accent, background: brandingForm.background, surface: brandingForm.surface, }, }, }; const updated = await updateTenantSettings(settings); const nextSettings = (updated.settings ?? {}) as Record; setTenantSettings(nextSettings); setBrandingForm(extractBrandingForm(nextSettings, TENANT_BRANDING_DEFAULTS)); setBrandingError(null); toast.success(t('profile.branding.updated', 'Standard-Branding gespeichert.')); } catch (err) { const message = getApiErrorMessage(err, brandingSaveError); setBrandingError(message); toast.error(message); } finally { setBrandingSaving(false); } }; const passwordReady = form.currentPassword.trim().length > 0 && form.password.trim().length > 0 && form.passwordConfirmation.trim().length > 0; const brandingDisabled = brandingLoading || brandingSaving; return ( {brandingTabEnabled ? ( setActiveTab('account')} /> setActiveTab('branding')} /> ) : null} {activeTab === 'branding' && brandingTabEnabled ? ( <> {brandingError ? ( {brandingError} ) : null} {t('profile.branding.title', 'Standard-Branding')} {t('profile.branding.hint', 'Diese Werte dienen als Vorschlag für neue Events und beschleunigen die Einrichtung.')} {t('profile.branding.theme', 'Theme')} setBrandingForm((prev) => ({ ...prev, mode: event.target.value as BrandingFormValues['mode'] }))} disabled={brandingDisabled} > setBrandingForm((prev) => ({ ...prev, fontSize: event.target.value as BrandingFormValues['fontSize'] }))} disabled={brandingDisabled} > {t('events.branding.colors', 'Colors')} setBrandingForm((prev) => ({ ...prev, primary: value }))} disabled={brandingDisabled} /> setBrandingForm((prev) => ({ ...prev, accent: value }))} disabled={brandingDisabled} /> setBrandingForm((prev) => ({ ...prev, background: value }))} disabled={brandingDisabled} /> setBrandingForm((prev) => ({ ...prev, surface: value }))} disabled={brandingDisabled} /> {t('events.branding.fonts', 'Fonts')} setBrandingForm((prev) => ({ ...prev, headingFont: event.target.value }))} placeholder={t('events.branding.headingFontPlaceholder', 'Playfair Display')} hasError={false} disabled={brandingDisabled} /> setBrandingForm((prev) => ({ ...prev, bodyFont: event.target.value }))} placeholder={t('events.branding.bodyFontPlaceholder', 'Montserrat')} hasError={false} disabled={brandingDisabled} /> ) : ( <> {error ? ( {error} ) : null} {form.name || profile?.email || t('profile.title', 'Profil')} {form.email || profile?.email || '—'} {profile?.email_verified ? ( ) : ( )} {emailStatusLabel} {emailHint} {t('profile.sections.account.heading', 'Account-Informationen')} {t('profile.sections.account.description', 'Passe Anzeigename, E-Mail-Adresse und Sprache der Admin-Oberfläche an.')} {loading ? ( {t('profile.loading', 'Lädt ...')} ) : ( setForm((prev) => ({ ...prev, name: event.target.value }))} placeholder={t('profile.placeholders.name', 'z. B. Hochzeitsplanung Schmidt')} hasError={false} /> setForm((prev) => ({ ...prev, email: event.target.value }))} placeholder="mail@beispiel.de" type="email" hasError={false} /> setForm((prev) => ({ ...prev, preferredLocale: event.target.value }))} > {LOCALE_OPTIONS.map((option) => ( ))} )} {t('profile.sections.password.heading', 'Passwort ändern')} {t('profile.sections.password.description', 'Wähle ein sicheres Passwort, um deinen Admin-Zugang zu schützen.')} setForm((prev) => ({ ...prev, currentPassword: event.target.value }))} placeholder="••••••••" type="password" hasError={false} /> setForm((prev) => ({ ...prev, password: event.target.value }))} placeholder="••••••••" type="password" hasError={false} /> setForm((prev) => ({ ...prev, passwordConfirmation: event.target.value }))} placeholder="••••••••" type="password" hasError={false} /> )} ); } function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) { const { primary, surfaceMuted, border, surface, text } = useAdminTheme(); return ( {label} ); } function ColorField({ label, value, onChange, disabled, }: { label: string; value: string; onChange: (next: string) => void; disabled?: boolean; }) { const { text, muted } = useAdminTheme(); return ( {label} onChange(event.target.value)} disabled={disabled} /> {value} ); }