2026-05-09 10:02:56 +02:00
import { useCallback , useEffect , useMemo , useState } from 'react'
2026-05-05 21:05:52 +02:00
import { Navigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import api from '../utils/api'
2026-05-09 10:42:56 +02:00
import { activeClubMemberships } from '../utils/activeClub'
2026-05-05 21:05:52 +02:00
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' } ,
]
2026-05-09 10:02:56 +02:00
const PORTAL _ROLE _LABEL = {
2026-05-05 21:05:52 +02:00
user : 'Nutzer' ,
2026-05-09 10:02:56 +02:00
trainer : 'Portal-Trainer' ,
admin : 'Portal-Administrator' ,
superadmin : 'Super-Administrator' ,
}
function clubAdminClubIds ( user ) {
2026-05-09 10:42:56 +02:00
return activeClubMemberships ( user ? . clubs )
2026-05-09 10:02:56 +02:00
. filter ( ( c ) => Array . isArray ( c . roles ) && c . roles . includes ( 'club_admin' ) )
. map ( ( c ) => c . id )
2026-05-05 21:05:52 +02:00
}
2026-05-09 10:02:56 +02:00
function clubSelectOptions ( user , allClubs , isPlatformAdmin ) {
if ( ! isPlatformAdmin ) {
const ids = new Set ( clubAdminClubIds ( user ) )
return ( allClubs || [ ] ) . filter ( ( c ) => ids . has ( c . id ) )
}
return allClubs || [ ]
}
/** Plattform-Rollen im UI (Tier/Abo entfällt bis auf Weiteres). */
2026-05-09 10:32:33 +02:00
function isEscalatedPortalRole ( role ) {
const r = ( role || 'user' ) . toLowerCase ( )
return r === 'admin' || r === 'superadmin'
}
2026-05-09 10:02:56 +02:00
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
}
export default function AdminUsersPage ( ) {
2026-05-05 21:05:52 +02:00
const { user } = useAuth ( )
const isPlatformAdmin = user ? . role === 'admin' || user ? . role === 'superadmin'
2026-05-09 10:02:56 +02:00
const isSuperadminViewer = user ? . role === 'superadmin'
const managedClubIds = useMemo ( ( ) => clubAdminClubIds ( user ) , [ user ] )
const clubOrgMode = ! isPlatformAdmin && managedClubIds . length > 0
const canAccess = isPlatformAdmin || clubOrgMode
2026-05-05 21:05:52 +02:00
2026-05-09 10:02:56 +02:00
const [ platformUsers , setPlatformUsers ] = useState ( [ ] )
2026-05-05 21:05:52 +02:00
const [ clubs , setClubs ] = useState ( [ ] )
2026-05-09 10:02:56 +02:00
const [ clubMembers , setClubMembers ] = useState ( [ ] )
const [ selectedClubId , setSelectedClubId ] = useState (
( ) => managedClubIds [ 0 ] ? ? null
)
2026-05-05 21:05:52 +02:00
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 )
2026-05-09 10:02:56 +02:00
const [ addMemberOpen , setAddMemberOpen ] = useState ( false )
const [ newMemberProfileId , setNewMemberProfileId ] = useState ( '' )
const [ newMemberRoles , setNewMemberRoles ] = useState ( [ 'trainer' ] )
2026-05-09 10:15:16 +02:00
const [ pwdModal , setPwdModal ] = useState ( null )
const [ pwdNew , setPwdNew ] = useState ( '' )
const [ pwdNew2 , setPwdNew2 ] = useState ( '' )
2026-05-05 21:05:52 +02:00
2026-05-09 10:02:56 +02:00
const selectableClubs = useMemo (
( ) => clubSelectOptions ( user , clubs , isPlatformAdmin ) ,
[ user , clubs , isPlatformAdmin ]
)
useEffect ( ( ) => {
if ( ! clubOrgMode ) return
if ( selectedClubId == null || ! managedClubIds . includes ( selectedClubId ) ) {
setSelectedClubId ( managedClubIds [ 0 ] ? ? null )
}
} , [ clubOrgMode , managedClubIds , selectedClubId ] )
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 ( ! canAccess ) return
if ( clubOrgMode ) 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 )
2026-05-05 21:05:52 +02:00
}
2026-05-09 10:02:56 +02:00
} ) ( )
return ( ) => {
cancelled = true
2026-05-05 21:05:52 +02:00
}
2026-05-09 10:02:56 +02:00
} , [ canAccess , clubOrgMode , loadPlatform ] )
2026-05-05 21:05:52 +02:00
useEffect ( ( ) => {
2026-05-09 10:02:56 +02:00
if ( ! canAccess || ! clubOrgMode || ! selectedClubId ) return
let cancelled = false
; ( async ( ) => {
setError ( '' )
setLoading ( true )
try {
const [ c , m ] = await Promise . all ( [
api . listClubs ( ) ,
api . listClubMembers ( selectedClubId , { includeInactive : true } ) ,
] )
if ( ! cancelled ) {
setClubs ( Array . isArray ( c ) ? c : [ ] )
setClubMembers ( Array . isArray ( m ) ? m : [ ] )
}
} catch ( e ) {
if ( ! cancelled ) setError ( e . message || String ( e ) )
} finally {
if ( ! cancelled ) setLoading ( false )
}
} ) ( )
return ( ) => {
cancelled = true
}
} , [ canAccess , clubOrgMode , selectedClubId ] )
2026-05-05 21:05:52 +02:00
2026-05-09 10:02:56 +02:00
const reloadClubMembers = useCallback ( async ( ) => {
if ( ! selectedClubId ) return
try {
const m = await api . listClubMembers ( selectedClubId , { includeInactive : true } )
setClubMembers ( Array . isArray ( m ) ? m : [ ] )
} catch {
setClubMembers ( [ ] )
}
} , [ selectedClubId ] )
if ( ! canAccess ) return < Navigate to = "/" replace / >
const selectedClubLabel =
selectableClubs . find ( ( c ) => c . id === selectedClubId ) ? . name || 'Verein'
2026-05-05 21:05:52 +02:00
const savePortal = async ( profileId ) => {
const dr = portalDraft [ profileId ]
if ( ! dr ) return
try {
2026-05-09 10:02:56 +02:00
await api . updateProfile ( profileId , { role : dr . role } )
await loadPlatform ( )
2026-05-05 21:05:52 +02:00
} 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' ] )
2026-05-09 10:02:56 +02:00
await loadPlatform ( )
2026-05-05 21:05:52 +02:00
} 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 )
2026-05-09 10:02:56 +02:00
if ( clubOrgMode ) await reloadClubMembers ( )
else await loadPlatform ( )
2026-05-05 21:05:52 +02:00
} catch ( e ) {
alert ( e . message || String ( e ) )
}
}
2026-05-09 10:42:56 +02:00
const toggleMemberClubAccess = async ( m , activate ) => {
if ( ! selectedClubId ) return
const st = activate ? 'active' : 'inactive'
if (
! activate &&
! confirm (
` Vereinszugang für „ ${ m . name || m . email || '#' + m . profile _id } “ in ${ selectedClubLabel } deaktivieren? ` +
'Die Person bleibt anmeldbar, sieht aber keine Inhalte dieses Vereins mehr (Login bleibt unverändert).'
)
) {
return
}
try {
await api . updateClubMember ( selectedClubId , m . profile _id , {
roles : [ ... ( m . roles || [ ] ) ] ,
status : st ,
} )
await reloadClubMembers ( )
} catch ( e ) {
alert ( e . message || String ( e ) )
}
}
2026-05-05 21:05:52 +02:00
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 )
2026-05-09 10:02:56 +02:00
if ( clubOrgMode ) await reloadClubMembers ( )
else await loadPlatform ( )
} catch ( e ) {
alert ( e . message || String ( e ) )
}
}
const submitAddClubMember = async ( ) => {
const raw = parseInt ( String ( newMemberProfileId ) . trim ( ) , 10 )
if ( ! Number . isFinite ( raw ) || raw < 1 ) {
alert ( 'Gültige Profil-ID eingeben.' )
return
}
if ( ! selectedClubId ) return
if ( ! newMemberRoles . length ) {
alert ( 'Mindestens eine Vereinsrolle.' )
return
}
try {
await api . addClubMember ( selectedClubId , { profile _id : raw , roles : newMemberRoles } )
setAddMemberOpen ( false )
setNewMemberProfileId ( '' )
setNewMemberRoles ( [ 'trainer' ] )
await reloadClubMembers ( )
2026-05-05 21:05:52 +02:00
} catch ( e ) {
alert ( e . message || String ( e ) )
}
}
2026-05-09 10:32:33 +02:00
const submitPasswordEmail = async ( ) => {
2026-05-09 10:15:16 +02:00
if ( ! pwdModal ) return
2026-05-09 10:32:33 +02:00
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 || ! isSuperadminViewer ) return
2026-05-09 10:15:16 +02:00
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 ( '' )
2026-05-09 10:32:33 +02:00
alert ( 'Neues Passwort wurde direkt gesetzt.' )
2026-05-09 10:15:16 +02:00
} catch ( e ) {
alert ( e . message || String ( e ) )
}
}
2026-05-05 21:05:52 +02:00
return (
< div className = "app-page" >
2026-05-09 10:02:56 +02:00
< AdminPageNav clubOrgOnly = { clubOrgMode } / >
< h1 style = { { marginTop : 0 } } > Nutzer & amp ; Vereinsrollen < / h1 >
{ clubOrgMode ? (
< p style = { { color : 'var(--text2)' , maxWidth : '52rem' , lineHeight : 1.55 , marginBottom : '1.25rem' } } >
2026-05-09 10:42:56 +02:00
Du verwaltest Mitglieder des ausgewählten Vereins . < strong > Vereinszugang deaktivieren < / strong > sperrt nur die
Sicht auf Vereinsinhalte — der Login des Nutzers bleibt möglich . Wiederherstellen über „ aktivieren “ oder
Bearbeiten .
2026-05-09 10:02:56 +02:00
< / p >
) : (
2026-05-09 10:15:16 +02:00
< >
< p style = { { color : 'var(--text2)' , maxWidth : '52rem' , lineHeight : 1.55 , marginBottom : '0.75rem' } } >
Gesamtübersicht aller Konten und Vereinszuordnungen . < strong > Portal < / strong > - Einstellungen steuern nur den
Zugang zur < strong > plattformweiten Administration < / strong > ( Kataloge , Hierarchie , globale Nutzerliste usw . ) ;
Vereinsarbeit ( Trainer , Vereinsadmin & nbsp ; … ) bleiben < strong > Vereinsrollen < / strong > . Abonnement / Tier ist
derzeit nicht freigeschaltet .
< / p >
< div
className = "card"
style = { {
marginBottom : '1.25rem' ,
padding : '0.85rem 1rem' ,
maxWidth : '52rem' ,
fontSize : '0.92rem' ,
lineHeight : 1.5 ,
color : 'var(--text2)' ,
} }
>
< strong style = { { color : 'var(--text1)' } } > Die vier Portal - Zugriffsstufen : < / strong >
< ul style = { { margin : '0.5rem 0 0' , paddingLeft : '1.2rem' } } >
< li >
< strong > Nutzer < / strong > — Standardnutzer ohne Plattform - Admin - Bereiche .
< / li >
< li >
< strong > Portal - Trainer ( Legacy ) < / strong > — ältere Kennzeichnung auf Profil - Ebene ; organisatorisch ist
„ Trainer “ in der Regel eine < strong > Vereinsrolle < / strong > .
< / li >
< li >
< strong > Portal - Administrator < / strong > — Zugang zu allen geschützten < code > / admin < / code > - Bereichen
( Außer : einige Funktionen nur Superadmin , z . & nbsp ; B . bestimmte Medien - / G o v e r n a n c e - A k t i o n e n ) .
< / li >
< li >
< strong > Super - Administrator < / strong > — volle Plattform - Governance ( u . & nbsp ; a . offizielle Medien ,
Superadmin - Rolle vergeben , harte Lifecycle - Aktionen an Medien ) .
< / li >
< / ul >
< / div >
< / >
2026-05-09 10:02:56 +02:00
) }
{ clubOrgMode && managedClubIds . length > 1 ? (
< div className = "form-row" style = { { maxWidth : '24rem' , marginBottom : '1rem' } } >
< label className = "form-label" > Verein für Verwaltung < / label >
< select
className = "form-input"
value = { selectedClubId ? ? '' }
onChange = { ( e ) => setSelectedClubId ( parseInt ( e . target . value , 10 ) ) }
>
{ selectableClubs . map ( ( c ) => (
< option key = { c . id } value = { c . id } >
{ c . name }
< / option >
) ) }
< / select >
< / div >
) : null }
2026-05-05 21:05:52 +02:00
{ loading ? (
< p style = { { color : 'var(--text2)' } } > Laden … < / p >
) : error ? (
< div className = "card" style = { { borderColor : 'var(--danger)' , color : 'var(--danger)' } } >
{ error }
< / div >
2026-05-09 10:02:56 +02:00
) : clubOrgMode ? (
2026-05-05 21:05:52 +02:00
< div style = { { display : 'grid' , gap : '1rem' } } >
2026-05-09 10:02:56 +02:00
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : '0.5rem' , alignItems : 'center' } } >
< button type = "button" className = "btn btn-primary" onClick = { ( ) => setAddMemberOpen ( true ) } >
Mitglied hinzufügen ( Profil - ID )
< / button >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => reloadClubMembers ( ) } >
Aktualisieren
< / button >
< / div >
{ ! clubMembers . length ? (
< p className = "muted" > Keine Mitglieder in diesem Verein . < / p >
) : (
2026-05-09 10:42:56 +02:00
clubMembers . map ( ( m ) => {
const memStatus = ( m . status || 'active' ) . toLowerCase ( )
return (
2026-05-09 10:02:56 +02:00
< div key = { m . membership _id } className = "card" >
< div style = { { display : 'flex' , justifyContent : 'space-between' , flexWrap : 'wrap' , gap : '0.75rem' } } >
2026-05-05 21:05:52 +02:00
< div >
2026-05-09 10:02:56 +02:00
< strong style = { { fontSize : '1.05rem' } } >
{ m . name || '—' } < span style = { { color : 'var(--text2)' , fontWeight : 400 } } > # { m . profile _id } < / span >
< / strong >
< div style = { { fontSize : '0.875rem' , color : 'var(--text2)' } } > { m . email || '—' } < / div >
< div style = { { fontSize : '0.78rem' , color : 'var(--text3)' , marginTop : '0.35rem' } } >
2026-05-09 10:42:56 +02:00
Vereinszugang : { ' ' }
< strong style = { { color : memStatus === 'active' ? 'var(--text1)' : 'var(--warning, #d4a012)' } } >
{ memStatus === 'active' ? 'aktiv' : 'deaktiviert' }
< / strong >
{ ' ' }
· Verifiziert : { m . email _verified ? 'ja' : 'nein' }
2026-05-09 10:02:56 +02:00
< / div >
< div style = { { fontSize : '0.82rem' , marginTop : '0.35rem' } } >
Rollen : { ( m . roles || [ ] ) . join ( ', ' ) || '—' }
< / div >
2026-05-05 21:05:52 +02:00
< / div >
2026-05-09 10:15:16 +02:00
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.35rem' , alignItems : 'stretch' } } >
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) =>
setClubEditModal ( {
clubId : selectedClubId ,
clubName : selectedClubLabel ,
profileId : m . profile _id ,
profileLabel : m . name || m . email ,
roles : [ ... ( m . roles || [ ] ) ] ,
2026-05-09 10:42:56 +02:00
status : memStatus ,
2026-05-09 10:15:16 +02:00
} )
}
>
Bearbeiten
< / button >
2026-05-09 10:42:56 +02:00
{ m . profile _id !== user ? . id ? (
memStatus === 'inactive' ? (
< button
type = "button"
className = "btn btn-primary"
onClick = { ( ) => toggleMemberClubAccess ( m , true ) }
>
Vereinszugang aktivieren
< / button >
) : (
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) => toggleMemberClubAccess ( m , false ) }
>
Vereinszugang deaktivieren
< / button >
)
) : null }
2026-05-09 10:32:33 +02:00
{ m . profile _id !== user ? . id && ! isEscalatedPortalRole ( m . portal _role ) ? (
2026-05-09 10:15:16 +02:00
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) =>
setPwdModal ( {
profileId : m . profile _id ,
label : m . name || m . email || ` Profil # ${ m . profile _id } ` ,
} )
}
>
2026-05-09 10:32:33 +02:00
Passwort - Link senden
2026-05-09 10:15:16 +02:00
< / button >
) : null }
< / div >
2026-05-05 21:05:52 +02:00
< / div >
< / div >
2026-05-09 10:42:56 +02:00
)
} )
2026-05-09 10:02:56 +02:00
) }
< / div >
) : (
< div style = { { display : 'grid' , gap : '1rem' } } >
{ platformUsers . map ( ( row ) => {
const portalRoleChoices = portalRoleSelectOptions ( isSuperadminViewer , row . role )
return (
< div key = { row . id } className = "card" >
< div style = { { display : 'flex' , justifyContent : 'space-between' , flexWrap : 'wrap' , gap : '0.75rem' } } >
< div >
< strong style = { { fontSize : '1.05rem' } } >
{ row . name || '—' } < span style = { { color : 'var(--text2)' , fontWeight : 400 } } > # { row . id } < / span >
< / strong >
< div style = { { fontSize : '0.875rem' , color : 'var(--text2)' } } > { row . email || '—' } < / div >
< div style = { { fontSize : '0.78rem' , color : 'var(--text3)' , marginTop : '0.35rem' } } >
Verifiziert : { row . email _verified ? 'ja' : 'nein' }
< / div >
< / div >
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : '0.5rem' , alignItems : 'flex-end' } } >
< div >
< label className = "form-label" style = { { fontSize : '0.75rem' } } >
Portal - Zugriff
< / label >
< select
className = "form-input"
style = { { minWidth : '200px' } }
value = { ( portalDraft [ row . id ] ? . role || row . role || 'user' ) . toLowerCase ( ) }
onChange = { ( e ) =>
setPortalDraft ( ( prev ) => ( {
... prev ,
[ row . id ] : { role : e . target . value } ,
} ) )
}
>
{ portalRoleChoices . map ( ( r ) => (
< option key = { r . value } value = { r . value } >
{ r . label }
< / option >
) ) }
< / select >
< / div >
2026-05-09 10:15:16 +02:00
< button
type = "button"
className = "btn btn-secondary"
disabled = { row . id === user ? . id }
title = { row . id === user ? . id ? 'Eigenes Passwort unter Einstellungen' : undefined }
onClick = { ( ) =>
setPwdModal ( {
profileId : row . id ,
label : row . name || row . email || ` # ${ row . id } ` ,
} )
}
>
2026-05-09 10:32:33 +02:00
Passwort / Link
2026-05-09 10:15:16 +02:00
< / button >
2026-05-09 10:02:56 +02:00
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => savePortal ( row . id ) } >
Portal speichern
< / button >
< button
type = "button"
className = "btn btn-primary"
disabled = { ! clubs . length }
title = { ! clubs . length ? 'Zuerst einen Verein anlegen' : undefined }
onClick = { ( ) => {
if ( ! clubs . length ) return
setAssignRoles ( [ 'trainer' ] )
setAssignModal ( {
profileId : row . id ,
profileLabel : row . name || row . email || ` # ${ row . id } ` ,
clubId : clubs [ 0 ] ? . id ? ? '' ,
} )
} }
>
Verein zuweisen
< / button >
< / div >
< / div >
2026-05-05 21:05:52 +02:00
2026-05-09 10:02:56 +02:00
< div style = { { marginTop : '1rem' , paddingTop : '0.75rem' , borderTop : '1px solid var(--border)' } } >
< strong style = { { fontSize : '0.85rem' } } > Vereinsmitgliedschaften < / strong >
{ ! row . clubs ? . length ? (
< p style = { { color : 'var(--text2)' , fontSize : '0.875rem' , margin : '0.35rem 0 0' } } >
Keine Zuordnung .
< / p >
) : (
< ul style = { { margin : '0.5rem 0 0' , paddingLeft : '1.2rem' , fontSize : '0.9rem' } } >
{ ( row . clubs || [ ] ) . map ( ( c ) => (
< li key = { c . id } style = { { marginBottom : '0.35rem' } } >
< strong > { c . name } < / strong >
{ c . abbreviation ? ` ( ${ c . abbreviation } ) ` : '' } — { ' ' }
{ ( c . roles || [ ] ) . join ( ', ' ) || '—' }
{ c . membership _status === 'inactive' ? (
< span style = { { color : 'var(--text3)' , fontSize : '0.8rem' } } > ( inaktiv ) < / span >
) : null } { ' ' }
< button
type = "button"
style = { {
marginLeft : '0.35rem' ,
fontSize : '0.75rem' ,
padding : '0.12rem 0.45rem' ,
borderRadius : '6px' ,
border : '1px solid var(--border)' ,
background : 'var(--surface2)' ,
cursor : 'pointer' ,
} }
onClick = { ( ) =>
setClubEditModal ( {
clubId : c . id ,
clubName : c . name ,
profileId : row . id ,
profileLabel : row . name || row . email ,
roles : [ ... ( c . roles || [ ] ) ] ,
status : ( c . membership _status || 'active' ) . toLowerCase ( ) ,
} )
}
>
bearbeiten
< / button >
< / li >
) ) }
< / ul >
) }
< / div >
2026-05-05 21:05:52 +02:00
< / div >
)
} ) }
< / div >
) }
{ assignModal && (
< div
style = { {
position : 'fixed' ,
inset : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1200 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '440px' ,
width : '100%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Verein zuweisen < / h2 >
< p style = { { color : 'var(--text2)' , fontSize : '0.9rem' } } > { assignModal . profileLabel } < / p >
< div className = "form-row" >
< label className = "form-label" > Verein < / label >
< select
className = "form-input"
value = { assignModal . clubId === '' ? '' : String ( assignModal . clubId ) }
onChange = { ( e ) =>
setAssignModal ( ( prev ) =>
prev ? { ... prev , clubId : e . target . value ? parseInt ( e . target . value , 10 ) : '' } : prev
)
}
>
{ clubs . map ( ( c ) => (
< option key = { c . id } value = { String ( c . id ) } >
{ c . name }
< / option >
) ) }
< / select >
< / div >
< div className = "form-row" >
< span className = "form-label" > Rollen im Verein < / span >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.35rem' } } >
{ CLUB _ROLE _OPTIONS . map ( ( opt ) => (
< label key = { opt . code } style = { { display : 'flex' , alignItems : 'center' , gap : '0.35rem' } } >
< input
type = "checkbox"
checked = { assignRoles . includes ( opt . code ) }
onChange = { ( ) => {
setAssignRoles ( ( prev ) => {
const s = new Set ( prev )
if ( s . has ( opt . code ) ) s . delete ( opt . code )
else s . add ( opt . code )
const out = Array . from ( s )
return out . length ? out : [ 'trainer' ]
} )
} }
/ >
{ opt . label }
< / label >
) ) }
< / div >
< / div >
< div style = { { display : 'flex' , gap : '0.5rem' , marginTop : '1rem' } } >
< button type = "button" className = "btn btn-primary" style = { { flex : 1 } } onClick = { submitAssignClub } >
Zuweisen
< / button >
2026-05-09 10:02:56 +02:00
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setAssignModal ( null ) } >
2026-05-05 21:05:52 +02:00
Abbrechen
< / button >
< / div >
< / div >
< / div >
) }
2026-05-09 10:02:56 +02:00
{ addMemberOpen && clubOrgMode ? (
< div
style = { {
position : 'fixed' ,
inset : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1200 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '440px' ,
width : '100%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Mitglied hinzufügen < / h2 >
< p className = "muted" style = { { fontSize : '0.9rem' } } >
Verein : < strong > { selectedClubLabel } < / strong >
< / p >
< div className = "form-row" >
< label className = "form-label" > Profil - ID < / label >
< input
className = "form-input"
inputMode = "numeric"
value = { newMemberProfileId }
onChange = { ( e ) => setNewMemberProfileId ( e . target . value ) }
placeholder = "z. B. 42"
/ >
< / div >
< div className = "form-row" >
< span className = "form-label" > Rollen < / span >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.35rem' } } >
{ CLUB _ROLE _OPTIONS . map ( ( opt ) => (
< label key = { opt . code } style = { { display : 'flex' , alignItems : 'center' , gap : '0.35rem' } } >
< input
type = "checkbox"
checked = { newMemberRoles . includes ( opt . code ) }
onChange = { ( ) => {
setNewMemberRoles ( ( prev ) => {
const s = new Set ( prev )
if ( s . has ( opt . code ) ) s . delete ( opt . code )
else s . add ( opt . code )
const out = Array . from ( s )
return out . length ? out : [ 'trainer' ]
} )
} }
/ >
{ opt . label }
< / label >
) ) }
< / div >
< / div >
< div style = { { display : 'flex' , gap : '0.5rem' , marginTop : '1rem' } } >
< button type = "button" className = "btn btn-primary" style = { { flex : 1 } } onClick = { submitAddClubMember } >
Hinzufügen
< / button >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setAddMemberOpen ( false ) } >
Abbrechen
< / button >
< / div >
< / div >
< / div >
) : null }
2026-05-05 21:05:52 +02:00
{ clubEditModal && (
< div
style = { {
position : 'fixed' ,
inset : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1200 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '440px' ,
width : '100%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Vereinsmitgliedschaft < / h2 >
< p style = { { color : 'var(--text2)' , fontSize : '0.9rem' } } >
{ clubEditModal . profileLabel } → { clubEditModal . clubName }
< / p >
2026-05-09 10:42:56 +02:00
< p className = "muted" style = { { fontSize : '0.82rem' , lineHeight : 1.45 } } >
„ Deaktiviert “ betrifft nur den Zugriff auf Inhalte dieses Vereins ; Login und andere Vereine bleiben
unberührt .
< / p >
2026-05-05 21:05:52 +02:00
< div className = "form-row" >
2026-05-09 10:42:56 +02:00
< label className = "form-label" > Vereinszugang < / label >
2026-05-05 21:05:52 +02:00
< select
className = "form-input"
value = { clubEditModal . status }
onChange = { ( e ) =>
setClubEditModal ( ( prev ) => ( prev ? { ... prev , status : e . target . value } : prev ) )
}
>
2026-05-09 10:42:56 +02:00
< option value = "active" > aktiv — sieht Vereinsinhalte < / option >
< option value = "inactive" > deaktiviert — kein Zugriff auf Vereinsinhalte < / option >
2026-05-05 21:05:52 +02:00
< / select >
< / div >
< div className = "form-row" >
< span className = "form-label" > Rollen < / span >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.35rem' } } >
{ CLUB _ROLE _OPTIONS . map ( ( opt ) => (
< label key = { opt . code } style = { { display : 'flex' , alignItems : 'center' , gap : '0.35rem' } } >
< input
type = "checkbox"
checked = { clubEditModal . roles . includes ( opt . code ) }
onChange = { ( ) => {
setClubEditModal ( ( prev ) => {
if ( ! prev ) return prev
const s = new Set ( prev . roles )
if ( s . has ( opt . code ) ) s . delete ( opt . code )
else s . add ( opt . code )
let roles = Array . from ( s )
if ( ! roles . length ) roles = [ 'trainer' ]
return { ... prev , roles }
} )
} }
/ >
{ opt . label }
< / label >
) ) }
< / div >
< / div >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.5rem' , marginTop : '1rem' } } >
< button type = "button" className = "btn btn-primary" onClick = { saveClubMembership } >
Speichern
< / button >
< button
type = "button"
className = "btn"
style = { { background : 'var(--danger)' , color : '#fff' , border : 'none' } }
onClick = { removeClubMembership }
>
Aus Verein entfernen
< / button >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setClubEditModal ( null ) } >
Schließen
< / button >
< / div >
< / div >
< / div >
) }
2026-05-09 10:15:16 +02:00
{ pwdModal ? (
< div
style = { {
position : 'fixed' ,
inset : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1250 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '440px' ,
width : '100%' ,
} }
>
2026-05-09 10:32:33 +02:00
< h2 style = { { marginTop : 0 } } > Passwort zurücksetzen < / h2 >
2026-05-09 10:15:16 +02:00
< p style = { { color : 'var(--text2)' , fontSize : '0.9rem' } } > { pwdModal . label } < / p >
< p className = "muted" style = { { fontSize : '0.82rem' , marginBottom : '0.75rem' } } >
2026-05-09 10:32:33 +02:00
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 .
2026-05-09 10:15:16 +02:00
< / p >
2026-05-09 10:32:33 +02:00
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.5rem' } } >
< button type = "button" className = "btn btn-primary" onClick = { submitPasswordEmail } >
Reset - Link per E - Mail senden
2026-05-09 10:15:16 +02:00
< / button >
2026-05-09 10:32:33 +02:00
< / div >
{ isSuperadminViewer ? (
< >
< hr style = { { margin : '1rem 0' , borderColor : 'var(--border, #333)' } } / >
< p className = "muted" style = { { fontSize : '0.82rem' , marginBottom : '0.5rem' } } >
Ausnahme : Passwort direkt setzen ( nur bei Bedarf ) . Das bisherige Passwort ist danach ungültig .
< / p >
< div className = "form-row" >
< label className = "form-label" > Neues Passwort < / label >
< input
type = "password"
className = "form-input"
autoComplete = "new-password"
value = { pwdNew }
onChange = { ( e ) => setPwdNew ( e . target . value ) }
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Wiederholen < / label >
< input
type = "password"
className = "form-input"
autoComplete = "new-password"
value = { pwdNew2 }
onChange = { ( e ) => setPwdNew2 ( e . target . value ) }
/ >
< / div >
< button type = "button" className = "btn btn-secondary" style = { { width : '100%' } } onClick = { submitPasswordDirect } >
Passwort direkt setzen
< / button >
< / >
) : null }
< div style = { { marginTop : '1rem' } } >
2026-05-09 10:15:16 +02:00
< button
type = "button"
className = "btn btn-secondary"
2026-05-09 10:32:33 +02:00
style = { { width : '100%' } }
2026-05-09 10:15:16 +02:00
onClick = { ( ) => {
setPwdModal ( null )
setPwdNew ( '' )
setPwdNew2 ( '' )
} }
>
2026-05-09 10:32:33 +02:00
Schließen
2026-05-09 10:15:16 +02:00
< / button >
< / div >
< / div >
< / div >
) : null }
2026-05-05 21:05:52 +02:00
< / div >
)
}