import React from 'react'; import { AlertTriangle, LogOut, Palette, UserCog } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import AppearanceToggleDropdown from '@/components/appearance-dropdown'; import { Button } from '@/components/ui/button'; import { CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { AdminLayout } from '../components/AdminLayout'; import { TenantHeroCard, FrostedCard, FrostedSurface, tenantHeroPrimaryButtonClass, tenantHeroSecondaryButtonClass, } from '../components/tenant'; import { useAuth } from '../auth/context'; import { ADMIN_EVENTS_PATH, ADMIN_LOGIN_PATH, ADMIN_PROFILE_PATH } from '../constants'; import { encodeReturnTo } from '../lib/returnTo'; import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences, NotificationPreferencesMeta, } from '../api'; import { getApiErrorMessage } from '../lib/apiError'; import { useTranslation } from 'react-i18next'; export default function SettingsPage() { const navigate = useNavigate(); const { user, logout } = useAuth(); const { t } = useTranslation('management'); const [preferences, setPreferences] = React.useState(null); const [defaults, setDefaults] = React.useState({}); const [loadingNotifications, setLoadingNotifications] = React.useState(true); const [savingNotifications, setSavingNotifications] = React.useState(false); const [notificationError, setNotificationError] = React.useState(null); const [notificationMeta, setNotificationMeta] = React.useState(null); const heroDescription = t('settings.hero.description', { defaultValue: 'Gestalte das Erlebnis für dein Admin-Team – Darstellung, Benachrichtigungen und Session-Sicherheit.' }); const heroSupporting = [ t('settings.hero.summary.appearance', { defaultValue: 'Synchronisiere den Look & Feel mit dem Gästeportal oder schalte den Dark Mode frei.' }), t('settings.hero.summary.notifications', { defaultValue: 'Stimme Benachrichtigungen auf Aufgaben, Pakete und Live-Events ab.' }) ]; const accountName = user?.name ?? user?.email ?? 'Tenant Admin'; const heroPrimaryAction = ( ); const heroSecondaryAction = ( ); const heroAside = (

{t('settings.hero.accountLabel', { defaultValue: 'Angemeldeter Account' })}

{accountName}

{user?.tenant_id ? (

Tenant #{user.tenant_id}

) : null}

{t('settings.hero.support', { defaultValue: 'Passe Einstellungen für dich und dein Team an – Änderungen wirken sofort im Admin.' })}

); const translateNotification = React.useCallback( (key: string, fallback?: string, options?: Record) => t(key, { defaultValue: fallback, ...(options ?? {}) }), [t], ); function handleLogout() { const target = new URL(ADMIN_LOGIN_PATH, window.location.origin); target.searchParams.set('reset-auth', '1'); target.searchParams.set('return_to', encodeReturnTo(ADMIN_EVENTS_PATH)); logout({ redirect: `${target.pathname}${target.search}` }); } React.useEffect(() => { (async () => { try { const result = await getNotificationPreferences(); setPreferences(result.preferences); setDefaults(result.defaults); setNotificationMeta(result.meta ?? null); } catch (error) { setNotificationError(getApiErrorMessage(error, t('settings.notifications.errorLoad', 'Benachrichtigungseinstellungen konnten nicht geladen werden.'))); } finally { setLoadingNotifications(false); } })(); }, [t]); return ( Darstellung & Account Gestalte den Admin-Bereich so farbenfroh wie dein Gästeportal.

Darstellung

Wechsel zwischen Hell- und Dunkelmodus oder übernimm automatisch die Systemeinstellung.

Angemeldeter Account

{user ? ( <> Eingeloggt als {user.name ?? user.email ?? 'Tenant Admin'} {user.tenant_id && <> - Tenant #{user.tenant_id}} ) : ( 'Aktuell kein Benutzer geladen.' )}

{t('settings.notifications.title', 'Benachrichtigungen')} {t('settings.notifications.description', 'Lege fest, für welche Ereignisse wir dich per E-Mail informieren.')} {notificationError && ( {notificationError} )} {loadingNotifications ? (
{Array.from({ length: 5 }).map((_, index) => ( ))}
) : preferences ? ( setPreferences(next)} onReset={() => setPreferences(defaults)} onSave={async () => { if (!preferences) { return; } try { setSavingNotifications(true); const updated = await updateNotificationPreferences(preferences); setPreferences(updated.preferences); if (updated.defaults && Object.keys(updated.defaults).length > 0) { setDefaults(updated.defaults); } if (updated.meta) { setNotificationMeta(updated.meta); } setNotificationError(null); } catch (error) { setNotificationError( getApiErrorMessage(error, t('settings.notifications.errorSave', 'Speichern fehlgeschlagen. Bitte versuche es erneut.')), ); } finally { setSavingNotifications(false); } }} saving={savingNotifications} translate={translateNotification} /> ) : null}
); } function NotificationPreferencesForm({ preferences, defaults, meta, onChange, onReset, onSave, saving, translate, }: { preferences: NotificationPreferences; defaults: NotificationPreferences; meta: NotificationPreferencesMeta | null; onChange: (next: NotificationPreferences) => void; onReset: () => void; onSave: () => Promise; saving: boolean; translate: (key: string, fallback?: string, options?: Record) => string; }) { const items = React.useMemo(() => buildPreferenceMeta(translate), [translate]); const locale = typeof window !== 'undefined' ? window.navigator.language : 'de-DE'; const creditText = React.useMemo(() => { if (!meta) { return null; } if (meta.credit_warning_sent_at) { const date = formatDateTime(meta.credit_warning_sent_at, locale); return translate('settings.notifications.meta.creditLast', 'Letzte Credit-Warnung: {{date}}', { date, }); } return translate('settings.notifications.meta.creditNever', 'Noch keine Credit-Warnung versendet.'); }, [meta, translate, locale]); return (
{items.map((item) => { const checked = preferences[item.key] ?? defaults[item.key] ?? true; return (

{item.label}

{item.description}

onChange({ ...preferences, [item.key]: Boolean(value) })} />
); })}
{translate('settings.notifications.hint', 'Du kannst Benachrichtigungen jederzeit wieder aktivieren.')}
{creditText ?

{creditText}

: null}
); } function buildPreferenceMeta( translate: (key: string, fallback?: string, options?: Record) => string ): Array<{ key: keyof NotificationPreferences; label: string; description: string }> { const map = [ { key: 'photo_thresholds', label: translate('settings.notifications.items.photoThresholds.label', 'Warnung bei Foto-Schwellen'), description: translate('settings.notifications.items.photoThresholds.description', 'Sende Warnungen bei 80 % und 95 % Foto-Auslastung.'), }, { key: 'photo_limits', label: translate('settings.notifications.items.photoLimits.label', 'Sperre bei Foto-Limit'), description: translate('settings.notifications.items.photoLimits.description', 'Informiere mich, sobald keine Foto-Uploads mehr möglich sind.'), }, { key: 'guest_thresholds', label: translate('settings.notifications.items.guestThresholds.label', 'Warnung bei Gästekontingent'), description: translate('settings.notifications.items.guestThresholds.description', 'Warnung kurz bevor alle Gästelinks vergeben sind.'), }, { key: 'guest_limits', label: translate('settings.notifications.items.guestLimits.label', 'Sperre bei Gästelimit'), description: translate('settings.notifications.items.guestLimits.description', 'Hinweis, wenn keine neuen Gästelinks mehr erzeugt werden können.'), }, { key: 'gallery_warnings', label: translate('settings.notifications.items.galleryWarnings.label', 'Galerie läuft bald ab'), description: translate('settings.notifications.items.galleryWarnings.description', 'Erhalte 7 und 1 Tag vor Ablauf eine Erinnerung.'), }, { key: 'gallery_expired', label: translate('settings.notifications.items.galleryExpired.label', 'Galerie ist abgelaufen'), description: translate('settings.notifications.items.galleryExpired.description', 'Informiere mich, sobald Gäste die Galerie nicht mehr sehen können.'), }, { key: 'event_thresholds', label: translate('settings.notifications.items.eventThresholds.label', 'Warnung bei Event-Kontingent'), description: translate('settings.notifications.items.eventThresholds.description', 'Hinweis, wenn das Reseller-Paket fast ausgeschöpft ist.'), }, { key: 'event_limits', label: translate('settings.notifications.items.eventLimits.label', 'Sperre bei Event-Kontingent'), description: translate('settings.notifications.items.eventLimits.description', 'Nachricht, sobald keine weiteren Events erstellt werden können.'), }, { key: 'package_expiring', label: translate('settings.notifications.items.packageExpiring.label', 'Paket läuft bald ab'), description: translate('settings.notifications.items.packageExpiring.description', 'Erinnerungen bei 30, 7 und 1 Tag vor Paketablauf.'), }, { key: 'package_expired', label: translate('settings.notifications.items.packageExpired.label', 'Paket ist abgelaufen'), description: translate('settings.notifications.items.packageExpired.description', 'Benachrichtige mich, wenn das Paket abgelaufen ist.'), }, { key: 'credits_low', label: translate('settings.notifications.items.creditsLow.label', 'Event-Credits werden knapp'), description: translate('settings.notifications.items.creditsLow.description', 'Informiert mich bei niedrigen Credit-Schwellen.'), }, ]; return map as Array<{ key: keyof NotificationPreferences; label: string; description: string }>; } function formatDateTime(value: string, locale: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } try { return new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', }).format(date); } catch { return date.toISOString(); } }