import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n' import Navbar from '../components/Layout/Navbar' import CustomSelect from '../components/shared/CustomSelect' import { useToast } from '../components/shared/Toast' import { Save, Map, Palette, User, Moon, Sun, Monitor, Shield, Camera, Trash2, Lock, KeyRound } from 'lucide-react' import { authApi, adminApi } from '../api/client' import type { LucideIcon } from 'lucide-react' import type { UserWithOidc } from '../types' import { getApiErrorMessage } from '../types' interface MapPreset { name: string url: string } const MAP_PRESETS: MapPreset[] = [ { name: 'OpenStreetMap', url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' }, { name: 'OpenStreetMap DE', url: 'https://tile.openstreetmap.de/{z}/{x}/{y}.png' }, { name: 'CartoDB Light', url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' }, { name: 'CartoDB Dark', url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' }, { name: 'Stadia Smooth', url: 'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png' }, ] interface SectionProps { title: string icon: LucideIcon children: React.ReactNode } function Section({ title, icon: Icon, children }: SectionProps): React.ReactElement { return (

{title}

{children}
) } export default function SettingsPage(): React.ReactElement { const { user, updateProfile, uploadAvatar, deleteAvatar, logout, loadUser, demoMode } = useAuthStore() const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const avatarInputRef = React.useRef(null) const { settings, updateSetting, updateSettings } = useSettingsStore() const { t, locale } = useTranslation() const toast = useToast() const navigate = useNavigate() const [saving, setSaving] = useState>({}) // Map settings const [mapTileUrl, setMapTileUrl] = useState(settings.map_tile_url || '') const [defaultLat, setDefaultLat] = useState(settings.default_lat || 48.8566) const [defaultLng, setDefaultLng] = useState(settings.default_lng || 2.3522) const [defaultZoom, setDefaultZoom] = useState(settings.default_zoom || 10) // Display const [tempUnit, setTempUnit] = useState(settings.temperature_unit || 'celsius') // Account const [username, setUsername] = useState(user?.username || '') const [email, setEmail] = useState(user?.email || '') const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [oidcOnlyMode, setOidcOnlyMode] = useState(false) useEffect(() => { authApi.getAppConfig?.().then((config) => { if (config?.oidc_only_mode) setOidcOnlyMode(true) }).catch(() => {}) }, []) const [mfaQr, setMfaQr] = useState(null) const [mfaSecret, setMfaSecret] = useState(null) const [mfaSetupCode, setMfaSetupCode] = useState('') const [mfaDisablePwd, setMfaDisablePwd] = useState('') const [mfaDisableCode, setMfaDisableCode] = useState('') const [mfaLoading, setMfaLoading] = useState(false) useEffect(() => { setMapTileUrl(settings.map_tile_url || '') setDefaultLat(settings.default_lat || 48.8566) setDefaultLng(settings.default_lng || 2.3522) setDefaultZoom(settings.default_zoom || 10) setTempUnit(settings.temperature_unit || 'celsius') }, [settings]) useEffect(() => { setUsername(user?.username || '') setEmail(user?.email || '') }, [user]) const saveMapSettings = async (): Promise => { setSaving(s => ({ ...s, map: true })) try { await updateSettings({ map_tile_url: mapTileUrl, default_lat: parseFloat(String(defaultLat)), default_lng: parseFloat(String(defaultLng)), default_zoom: parseInt(String(defaultZoom)), }) toast.success(t('settings.toast.mapSaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') } finally { setSaving(s => ({ ...s, map: false })) } } const saveDisplay = async (): Promise => { setSaving(s => ({ ...s, display: true })) try { await updateSetting('temperature_unit', tempUnit) toast.success(t('settings.toast.displaySaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') } finally { setSaving(s => ({ ...s, display: false })) } } const handleAvatarUpload = async (e: React.ChangeEvent): Promise => { const file = e.target.files?.[0] if (!file) return try { await uploadAvatar(file) toast.success(t('settings.avatarUploaded')) } catch { toast.error(t('settings.avatarError')) } if (avatarInputRef.current) avatarInputRef.current.value = '' } const handleAvatarRemove = async (): Promise => { try { await deleteAvatar() toast.success(t('settings.avatarRemoved')) } catch { toast.error(t('settings.avatarError')) } } const saveProfile = async (): Promise => { setSaving(s => ({ ...s, profile: true })) try { await updateProfile({ username, email }) toast.success(t('settings.toast.profileSaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') } finally { setSaving(s => ({ ...s, profile: false })) } } return (

{t('settings.title')}

{t('settings.subtitle')}

{/* Map settings */}
{ if (value) setMapTileUrl(value) }} placeholder={t('settings.mapTemplatePlaceholder.select')} options={MAP_PRESETS.map(p => ({ value: p.url, label: p.name, }))} size="sm" style={{ marginBottom: 8 }} /> ) => setMapTileUrl(e.target.value)} placeholder="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />

{t('settings.mapDefaultHint')}

) => setDefaultLat(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
) => setDefaultLng(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
{/* Display */}
{/* Dark Mode Toggle */}
{[ { value: 'light', label: t('settings.light'), icon: Sun }, { value: 'dark', label: t('settings.dark'), icon: Moon }, { value: 'auto', label: t('settings.auto'), icon: Monitor }, ].map(opt => { const current = settings.dark_mode const isActive = current === opt.value || (opt.value === 'light' && current === false) || (opt.value === 'dark' && current === true) return ( ) })}
{/* Sprache */}
{SUPPORTED_LANGUAGES.map(opt => ( ))}
{/* Temperature */}
{[ { value: 'celsius', label: '°C Celsius' }, { value: 'fahrenheit', label: '°F Fahrenheit' }, ].map(opt => ( ))}
{/* Zeitformat */}
{[ { value: '24h', label: '24h (14:30)' }, { value: '12h', label: '12h (2:30 PM)' }, ].map(opt => ( ))}
{/* Route Calculation */}
{[ { value: true, label: t('settings.on') || 'On' }, { value: false, label: t('settings.off') || 'Off' }, ].map(opt => ( ))}
{/* Account */}
) => setUsername(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
) => setEmail(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
{/* Change Password */} {!oidcOnlyMode && (
) => setCurrentPassword(e.target.value)} placeholder={t('settings.currentPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" /> ) => setNewPassword(e.target.value)} placeholder={t('settings.newPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" /> ) => setConfirmPassword(e.target.value)} placeholder={t('settings.confirmPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
)} {/* MFA */}

{t('settings.mfa.title')}

{t('settings.mfa.description')}

{demoMode ? (

{t('settings.mfa.demoBlocked')}

) : ( <>

{user?.mfa_enabled ? t('settings.mfa.enabled') : t('settings.mfa.disabled')}

{!user?.mfa_enabled && !mfaQr && ( )} {!user?.mfa_enabled && mfaQr && (

{t('settings.mfa.scanQr')}

{mfaSecret}
setMfaSetupCode(e.target.value.replace(/\D/g, '').slice(0, 8))} placeholder={t('settings.mfa.codePlaceholder')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
)} {user?.mfa_enabled && (

{t('settings.mfa.disableTitle')}

{t('settings.mfa.disableHint')}

setMfaDisablePwd(e.target.value)} placeholder={t('settings.currentPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> setMfaDisableCode(e.target.value.replace(/\D/g, '').slice(0, 8))} placeholder={t('settings.mfa.codePlaceholder')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
)} )}
{user?.avatar_url ? ( ) : (
{user?.username?.charAt(0).toUpperCase()}
)} {user?.avatar_url && ( )}
{user?.role === 'admin' ? <> {t('settings.roleAdmin')} : t('settings.roleUser')} {(user as UserWithOidc)?.oidc_issuer && ( SSO )}
{(user as UserWithOidc)?.oidc_issuer && (

{t('settings.oidcLinked')} {(user as UserWithOidc).oidc_issuer!.replace('https://', '').replace(/\/+$/, '')}

)}
{/* Delete Account Confirmation */} {showDeleteConfirm === 'blocked' && (
setShowDeleteConfirm(false)}>
) => e.stopPropagation()}>

{t('settings.deleteBlockedTitle')}

{t('settings.deleteBlockedMessage')}

)} {showDeleteConfirm === true && (
setShowDeleteConfirm(false)}>
) => e.stopPropagation()}>

{t('settings.deleteAccountTitle')}

{t('settings.deleteAccountWarning')}

)}
) }