import { useState, useEffect } from 'react' import { Link } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import { PLANNING_MODULE_DISPLAY_MODES, resolvePlanningModuleDisplayMode, } from '../config/planningModuleUx' import api from '../utils/api' import TotpSetupCard from '../components/TotpSetupCard' /** * Persönliche Einstellungen (Anzeige/Name, Kontostatus, Passwort). */ function AccountSettingsPage() { const { user, checkAuth } = useAuth() const [name, setName] = useState('') const [savingProfile, setSavingProfile] = useState(false) const [publicClubsDir, setPublicClubsDir] = useState([]) const [myJoinRequests, setMyJoinRequests] = useState([]) const [joinClubId, setJoinClubId] = useState('') const [joinMessage, setJoinMessage] = useState('') const [joinBusy, setJoinBusy] = useState(false) const [planningPrefsBusy, setPlanningPrefsBusy] = useState(false) /** @type {[string, React.Dispatch>]} */ const [moduleDisplayDraft, setModuleDisplayDraft] = useState( PLANNING_MODULE_DISPLAY_MODES.COMPACT_TAG_LEGEND ) const [currentPw, setCurrentPw] = useState('') const [disablePw, setDisablePw] = useState('') const [disableCode, setDisableCode] = useState('') const [disablingTotp, setDisablingTotp] = useState(false) const [freshRecoveryCodes, setFreshRecoveryCodes] = useState([]) const [newPw1, setNewPw1] = useState('') const [newPw2, setNewPw2] = useState('') const [savingPw, setSavingPw] = useState(false) const [resendingVerify, setResendingVerify] = useState(false) const [message, setMessage] = useState('') const [error, setError] = useState('') useEffect(() => { setName(typeof user?.name === 'string' ? user.name : '') }, [user]) useEffect(() => { const m = resolvePlanningModuleDisplayMode(user?.training_planning_prefs?.module_display_mode) setModuleDisplayDraft(m) }, [user?.id, user?.training_planning_prefs]) const refreshJoinRequests = () => { api.getMyClubJoinRequests().then(setMyJoinRequests).catch(() => {}) } useEffect(() => { if (!user?.id) return api.listPublicClubsDirectory().then(setPublicClubsDir).catch(() => {}) refreshJoinRequests() }, [user?.id]) const memberClubIds = new Set((user?.clubs || []).map((c) => c.id)) const pendingClubIds = new Set( myJoinRequests.filter((r) => r.status === 'pending').map((r) => r.club_id) ) const joinClubChoices = publicClubsDir.filter( (c) => !memberClubIds.has(c.id) && !pendingClubIds.has(c.id) ) const joinStatusLabel = (s) => ({ pending: 'ausstehend', accepted: 'angenommen', rejected: 'abgelehnt', withdrawn: 'zurückgezogen', })[s] || s /** API: boolean true / Legacy: fehlt oder false → als „nicht verifiziert“ behandeln */ const emailExplicitlyVerified = user?.email_verified === true || user?.email_verified === 't' || user?.email_verified === 1 || user?.email_verified === 'true' const showOk = (text) => { setMessage(text) setError('') setTimeout(() => setMessage(''), 5000) } const showErr = (text) => { setError(text) setMessage('') } const handleSaveName = async (e) => { e.preventDefault() if (!user?.id) return const trimmed = (name || '').trim() if (trimmed.length < 2) { showErr('Name sollte mindestens 2 Zeichen haben.') return } setSavingProfile(true) try { await api.updateProfile(user.id, { name: trimmed }) await checkAuth() showOk('Profilname gespeichert.') } catch (err) { showErr(err.message || 'Speichern fehlgeschlagen.') } finally { setSavingProfile(false) } } const handleSavePlanningPrefs = async (e) => { e.preventDefault() if (!user?.id) return setPlanningPrefsBusy(true) try { const base = user.training_planning_prefs && typeof user.training_planning_prefs === 'object' ? { ...user.training_planning_prefs } : {} const merged = { ...base, module_display_mode: moduleDisplayDraft } await api.updateProfile(user.id, { training_planning_prefs: merged }) await checkAuth() showOk('Planungs-Anzeige gespeichert.') } catch (err) { showErr(err.message || 'Konnte nicht speichern.') } finally { setPlanningPrefsBusy(false) } } const handleResendVerification = async () => { const em = user?.email if (!em) return setResendingVerify(true) try { await api.resendVerification(em) showOk('Falls diese Adresse einen unbestätigten Account hat: E-Mail ist unterwegs — Postfach prüfen.') } catch (err) { showErr(err.message || 'Konnte keine E-Mail senden.') } finally { setResendingVerify(false) } } const handleChangePassword = async (e) => { e.preventDefault() if (newPw1.length < 8) { showErr('Neues Passwort: mindestens 8 Zeichen.') return } if (newPw1 !== newPw2) { showErr('Die Passwörter stimmen nicht überein.') return } setSavingPw(true) try { const result = await api.changePassword(currentPw, newPw1) if (result?.token) localStorage.setItem('authToken', result.token) setCurrentPw('') setNewPw1('') setNewPw2('') showOk('Passwort aktualisiert. Andere Geräte sind abgemeldet.') } catch (err) { showErr(err.message || 'Passwort konnte nicht geändert werden.') } finally { setSavingPw(false) } } return (

Einstellungen

Konto & Sicherheit

{message && (
{message}
)} {error && (
{error}
)} {user?.totp_enabled ? (

Zwei-Faktor ist aktiv

Beim Anmelden wird zusätzlich der Code aus der Authenticator-App verlangt. Die ersten Codes lassen sich nicht erneut anzeigen. Hier entstehen neue, die alten gelten danach nicht mehr.

{freshRecoveryCodes.length > 0 ? (