import { useCallback, useEffect, useState } from 'react' import { Navigate } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import api from '../utils/api' import AdminPageNav from '../components/AdminPageNav' const CLUB_ROLE_OPTIONS = [ { code: 'club_admin', label: 'Vereinsadmin' }, { code: 'trainer', label: 'Trainer' }, { code: 'division_lead', label: 'Spartenleitung' }, { code: 'content_editor', label: 'Inhalte bearbeiten' }, ] const PORTAL_ROLE_LABEL = { user: 'Nutzer', trainer: 'Portal-Trainer', admin: 'Portal-Administrator', superadmin: 'Super-Administrator', } function portalRoleSelectOptions(viewerIsSuperadmin, currentRole) { const base = [ { value: 'user', label: PORTAL_ROLE_LABEL.user }, { value: 'trainer', label: `${PORTAL_ROLE_LABEL.trainer} (Legacy)` }, { value: 'admin', label: PORTAL_ROLE_LABEL.admin }, ] const cur = (currentRole || 'user').toLowerCase() if (viewerIsSuperadmin) base.push({ value: 'superadmin', label: PORTAL_ROLE_LABEL.superadmin }) const values = new Set(base.map((x) => x.value)) if (cur && !values.has(cur)) { base.unshift({ value: cur, label: cur }) } return base } /** * Nur Super-Admins — globale Nutzer- und Portal-Rollen plus Vereinszuordnung pro Profil. * Vereinsorganisation (Mitglieder, Anträge, Rollen vor Ort): unter Vereine → Mitglieder. */ export default function AdminUsersPage() { const { user } = useAuth() const isSuperadminViewer = user?.role === 'superadmin' const [platformUsers, setPlatformUsers] = useState([]) const [clubs, setClubs] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [portalDraft, setPortalDraft] = useState({}) const [assignModal, setAssignModal] = useState(null) const [assignRoles, setAssignRoles] = useState(['trainer']) const [clubEditModal, setClubEditModal] = useState(null) const [pwdModal, setPwdModal] = useState(null) const [pwdNew, setPwdNew] = useState('') const [pwdNew2, setPwdNew2] = useState('') const loadPlatform = useCallback(async () => { const [u, c] = await Promise.all([api.listAdminUsers(), api.listClubs()]) setPlatformUsers(Array.isArray(u) ? u : []) setClubs(Array.isArray(c) ? c : []) const d = {} for (const row of u || []) { d[row.id] = { role: (row.role || 'user').toLowerCase() } } setPortalDraft(d) }, []) useEffect(() => { if (!isSuperadminViewer) return let cancelled = false ;(async () => { setError('') setLoading(true) try { await loadPlatform() } catch (e) { if (!cancelled) setError(e.message || String(e)) } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, [isSuperadminViewer, loadPlatform]) if (!isSuperadminViewer) return const savePortal = async (profileId) => { const dr = portalDraft[profileId] if (!dr) return try { await api.updateProfile(profileId, { role: dr.role }) await loadPlatform() } catch (e) { alert(e.message || String(e)) } } const submitAssignClub = async () => { if (!assignModal) return const clubId = assignModal.clubId const profileId = assignModal.profileId if (!clubId || !assignRoles.length) { alert('Verein und mindestens eine Rolle wählen.') return } try { await api.addClubMember(clubId, { profile_id: profileId, roles: assignRoles }) setAssignModal(null) setAssignRoles(['trainer']) await loadPlatform() } catch (e) { alert(e.message || String(e)) } } const saveClubMembership = async () => { if (!clubEditModal) return const { clubId, profileId, roles, status } = clubEditModal try { await api.updateClubMember(clubId, profileId, { roles, status }) setClubEditModal(null) await loadPlatform() } catch (e) { alert(e.message || String(e)) } } const removeClubMembership = async () => { if (!clubEditModal) return if (!confirm('Mitgliedschaft in diesem Verein wirklich entfernen?')) return try { await api.removeClubMember(clubEditModal.clubId, clubEditModal.profileId) setClubEditModal(null) await loadPlatform() } catch (e) { alert(e.message || String(e)) } } const submitPasswordEmail = async () => { if (!pwdModal) return try { const res = await api.managementPasswordReset(pwdModal.profileId, null) setPwdModal(null) setPwdNew('') setPwdNew2('') let msg = 'Sofern eine E-Mail-Adresse hinterlegt ist, wurde ein Link zum Setzen eines neuen Passworts versendet. Das bisherige Passwort bleibt bis zur Bestätigung im Link aktiv.' if (res?.email_sent === false) { msg += ' Hinweis: Der E-Mail-Versand ist fehlgeschlagen (SMTP prüfen).' } alert(msg) } catch (e) { alert(e.message || String(e)) } } const submitPasswordDirect = async () => { if (!pwdModal) return if (pwdNew.length < 8) { alert('Mindestens 8 Zeichen.') return } if (pwdNew !== pwdNew2) { alert('Die beiden Passwörter stimmen nicht überein.') return } try { await api.managementPasswordReset(pwdModal.profileId, pwdNew) setPwdModal(null) setPwdNew('') setPwdNew2('') alert('Neues Passwort wurde direkt gesetzt.') } catch (e) { alert(e.message || String(e)) } } return (

Globale Nutzer & Portal-Rollen

<>

Plattformweite Konten und Vereinszuordnungen im Überblick. Vereinsmitgliedschaft vor Ort{' '} (Mitglieder, Beitrittsanträge, Vereinszugriffe) liegt unter Vereine → Mitglieder. Die geschützten /admin-Menüpunkte (Hierarchie, Kataloge, …) gibt es nur noch hier für Super-Admins.

Portal-Rollen auf Profil-Ebene:
  • Nutzer — Standard ohne globale Administrationsbereiche.
  • Portal-Trainer (Legacy) — historische Kennzeichnung; organisatorisch ist „Trainer“ meist eine{' '} Vereinsrolle.
  • Portal-Administrator — erhöhte API-/Operativ-Rechte nach Backend-Policies; die{' '} Admin-Navigation dieser App sieht jedoch nur noch der Super-Admin.
  • Super-Administrator — volle Plattform-Governance (diese Oberfläche, offizielle Inhalte, …).
{loading ? (

Laden…

) : error ? (
{error}
) : (
{platformUsers.map((row) => { const portalRoleChoices = portalRoleSelectOptions(isSuperadminViewer, row.role) return (
{row.name || '—'} #{row.id}
{row.email || '—'}
Verifiziert: {row.email_verified ? 'ja' : 'nein'}
Vereinsmitgliedschaften {!row.clubs?.length ? (

Keine Zuordnung.

) : (
    {(row.clubs || []).map((c) => (
  • {c.name} {c.abbreviation ? ` (${c.abbreviation})` : ''} —{' '} {(c.roles || []).join(', ') || '—'} {c.membership_status === 'inactive' ? ( {' '} (Vereinszugang deaktiviert) ) : null}{' '} {c.membership_status === 'inactive' ? ( ) : null}
  • ))}
)}
) })}
)} {assignModal && (

Verein zuweisen

{assignModal.profileLabel}

Rollen im Verein
{CLUB_ROLE_OPTIONS.map((opt) => ( ))}
)} {clubEditModal && (

Vereinsmitgliedschaft

{clubEditModal.profileLabel} → {clubEditModal.clubName}

„Deaktiviert“ betrifft nur den Zugriff auf Inhalte dieses Vereins; Login und andere Vereine bleiben unberührt.

Rollen
{CLUB_ROLE_OPTIONS.map((opt) => ( ))}
)} {pwdModal ? (

Passwort zurücksetzen

{pwdModal.label}

Standard: Es wird ein sicherer Link per E-Mail verschickt (wie „Passwort vergessen“). Das bisherige Passwort bleibt gültig, bis die Person den Link nutzt und ein neues Passwort wählt.

<>

Ausnahme: Passwort direkt setzen (nur bei Bedarf). Das bisherige Passwort ist danach ungültig.

setPwdNew(e.target.value)} />
setPwdNew2(e.target.value)} />
) : null}
) }