2026-05-06 12:49:35 +02:00
import React , { useState , useEffect , useMemo } from 'react'
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
import api from '../utils/api'
2026-05-09 09:13:38 +02:00
import { notifyOrgInboxChanged } from '../context/OrgInboxContext'
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
import { useAuth } from '../context/AuthContext'
2026-05-09 10:42:56 +02:00
import { activeClubMemberships } from '../utils/activeClub'
2026-05-09 13:26:22 +02:00
import { isEscalatedPortalRole } from '../utils/portalRoles'
2026-05-06 12:49:35 +02:00
import PageSectionNav from '../components/PageSectionNav'
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
2026-05-05 16:40:49 +02:00
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-04-22 06:48:18 +02:00
function ClubsPage ( ) {
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
const { user } = useAuth ( )
const [ activeTab , setActiveTab ] = useState ( 'clubs' )
const [ clubs , setClubs ] = useState ( [ ] )
const [ divisions , setDivisions ] = useState ( [ ] )
const [ groups , setGroups ] = useState ( [ ] )
const [ loading , setLoading ] = useState ( true )
const [ showModal , setShowModal ] = useState ( false )
const [ editing , setEditing ] = useState ( null )
const [ modalType , setModalType ] = useState ( 'club' )
2026-05-05 16:40:49 +02:00
const [ membersAdminClubId , setMembersAdminClubId ] = useState ( null )
const [ clubMembersAdmin , setClubMembersAdmin ] = useState ( [ ] )
const [ joinRequestsAdmin , setJoinRequestsAdmin ] = useState ( [ ] )
const [ membersAdminLoading , setMembersAdminLoading ] = useState ( false )
const [ groupMemberDirectory , setGroupMemberDirectory ] = useState ( [ ] )
const [ showAddMemberModal , setShowAddMemberModal ] = useState ( false )
const [ profilesAdminList , setProfilesAdminList ] = useState ( [ ] )
const [ addMemberForm , setAddMemberForm ] = useState ( { profile _id : '' , roles : [ 'trainer' ] } )
const [ editMemberModal , setEditMemberModal ] = useState ( null )
const [ acceptJoinModal , setAcceptJoinModal ] = useState ( null )
2026-05-09 13:26:22 +02:00
const [ pwdModal , setPwdModal ] = useState ( null )
const [ pwdNew , setPwdNew ] = useState ( '' )
const [ pwdNew2 , setPwdNew2 ] = useState ( '' )
2026-05-05 16:40:49 +02:00
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
// Form state
const [ formData , setFormData ] = useState ( { } )
2026-05-05 16:12:17 +02:00
const isPlatformAdmin = user ? . role === 'admin' || user ? . role === 'superadmin'
const isSuperAdmin = user ? . role === 'superadmin'
const clubAdminClubIds = new Set (
2026-05-09 10:42:56 +02:00
activeClubMemberships ( user ? . clubs )
2026-05-05 16:12:17 +02:00
. filter ( ( c ) => ( c . roles || [ ] ) . includes ( 'club_admin' ) )
. map ( ( c ) => c . id )
)
const canManageClub = ( clubId ) => isPlatformAdmin || clubAdminClubIds . has ( clubId )
const canCreateClub = isPlatformAdmin
const canManageOrgSomewhere = isPlatformAdmin || clubAdminClubIds . size > 0
const canCreateTrainingGroup =
2026-05-09 10:42:56 +02:00
isPlatformAdmin || activeClubMemberships ( user ? . clubs ) . length > 0
2026-05-05 16:12:17 +02:00
const canEditGroup = ( g ) =>
isPlatformAdmin ||
clubAdminClubIds . has ( g . club _id ) ||
g . trainer _id === user ? . id ||
( Array . isArray ( g . co _trainer _ids ) && g . co _trainer _ids . includes ( user ? . id ) )
const canDeleteGroup = ( g ) => isPlatformAdmin || clubAdminClubIds . has ( g . club _id )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
useEffect ( ( ) => {
loadData ( )
} , [ ] )
const loadData = async ( ) => {
try {
const [ clubsData , divisionsData , groupsData ] = await Promise . all ( [
api . listClubs ( ) ,
api . listDivisions ( ) ,
api . listTrainingGroups ( )
] )
setClubs ( clubsData )
setDivisions ( divisionsData )
setGroups ( groupsData )
} catch ( err ) {
console . error ( 'Failed to load data:' , err )
alert ( 'Fehler beim Laden: ' + err . message )
} finally {
setLoading ( false )
}
}
2026-05-05 16:40:49 +02:00
useEffect ( ( ) => {
if ( ! clubs . length ) {
setMembersAdminClubId ( null )
return
}
const mcl = clubs . filter ( ( c ) => isPlatformAdmin || clubAdminClubIds . has ( c . id ) )
if ( ! mcl . length ) {
setMembersAdminClubId ( null )
return
}
setMembersAdminClubId ( ( prev ) =>
prev != null && mcl . some ( ( x ) => x . id === prev ) ? prev : mcl [ 0 ] . id
)
} , [ clubs , isPlatformAdmin , user ? . clubs ] )
useEffect ( ( ) => {
if ( activeTab !== 'members' || ! membersAdminClubId || ! canManageClub ( membersAdminClubId ) ) return
let cancelled = false
setMembersAdminLoading ( true )
Promise . all ( [
api . listClubMembers ( membersAdminClubId , { includeInactive : true } ) ,
api . listClubJoinRequests ( membersAdminClubId ) ,
] )
. then ( ( [ m , j ] ) => {
if ( ! cancelled ) {
setClubMembersAdmin ( m )
setJoinRequestsAdmin ( j )
}
} )
. catch ( ( err ) => {
if ( ! cancelled ) alert ( 'Mitglieder/Anträge: ' + err . message )
} )
. finally ( ( ) => {
if ( ! cancelled ) setMembersAdminLoading ( false )
} )
return ( ) => {
cancelled = true
}
} , [ activeTab , membersAdminClubId ] )
useEffect ( ( ) => {
if ( ! showModal || modalType !== 'group' || ! formData . club _id ) {
setGroupMemberDirectory ( [ ] )
return
}
let cancelled = false
api
. clubMembersDirectory ( formData . club _id )
. then ( ( rows ) => {
if ( ! cancelled ) setGroupMemberDirectory ( rows )
} )
. catch ( ( ) => {
if ( ! cancelled ) setGroupMemberDirectory ( [ ] )
} )
return ( ) => {
cancelled = true
}
} , [ showModal , modalType , formData . club _id ] )
useEffect ( ( ) => {
if ( ! showAddMemberModal || ! isPlatformAdmin ) return
api . listProfiles ( ) . then ( setProfilesAdminList ) . catch ( ( ) => setProfilesAdminList ( [ ] ) )
} , [ showAddMemberModal , isPlatformAdmin ] )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
const handleCreate = ( type ) => {
setEditing ( null )
setModalType ( type )
if ( type === 'club' ) {
2026-05-05 16:12:17 +02:00
setFormData ( {
name : '' ,
abbreviation : '' ,
description : '' ,
status : 'active' ,
primary _admin _profile _id : user ? . id ? ? '' ,
} )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
} else if ( type === 'division' ) {
setFormData ( { club _id : '' , name : '' , focus _area : '' } )
} else if ( type === 'group' ) {
setFormData ( {
club _id : '' ,
division _id : '' ,
name : '' ,
focus : '' ,
level : '' ,
age _group : '' ,
weekday : '' ,
time _start : '' ,
time _end : '' ,
location : '' ,
2026-04-28 19:46:09 +02:00
trainer _id : user ? . id ? ? '' ,
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
co _trainer _ids : [ ] ,
status : 'active'
} )
}
setShowModal ( true )
}
2026-05-05 16:40:49 +02:00
const manageableClubs = clubs . filter ( ( c ) => canManageClub ( c . id ) )
const reloadMembersAdmin = async ( ) => {
if ( ! membersAdminClubId || ! canManageClub ( membersAdminClubId ) ) return
try {
const [ m , j ] = await Promise . all ( [
api . listClubMembers ( membersAdminClubId , { includeInactive : true } ) ,
api . listClubJoinRequests ( membersAdminClubId ) ,
] )
setClubMembersAdmin ( m )
setJoinRequestsAdmin ( j )
} catch ( err ) {
console . error ( err )
}
}
2026-05-09 10:55:58 +02:00
const toggleMembersAdminClubAccess = async ( m , activate ) => {
if ( ! membersAdminClubId || ! canManageClub ( membersAdminClubId ) ) return
const st = activate ? 'active' : 'inactive'
if (
! activate &&
! confirm (
` Vereinszugang für „ ${ m . name || m . email || '#' + m . profile _id } “ hier deaktivieren? Login bleibt möglich, Vereinsinhalte nicht — auch bei Super-Admins. ` ,
)
) {
return
}
try {
await api . updateClubMember ( membersAdminClubId , m . profile _id , {
roles : [ ... ( m . roles || [ ] ) ] ,
status : st ,
} )
await reloadMembersAdmin ( )
} catch ( err ) {
alert ( err . message || String ( err ) )
}
}
2026-05-09 13:26:22 +02:00
const submitMembersPasswordEmail = 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: E-Mail-Versand fehlgeschlagen (SMTP).'
alert ( msg )
} catch ( e ) {
alert ( e . message || String ( e ) )
}
}
const submitMembersPasswordDirect = async ( ) => {
if ( ! pwdModal || ! isSuperAdmin ) 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 ) )
}
}
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
const handleEdit = ( item , type ) => {
setEditing ( item )
setModalType ( type )
2026-05-05 16:40:49 +02:00
if ( type === 'group' ) {
const co = item . co _trainer _ids
setFormData ( {
... item ,
co _trainer _ids : Array . isArray ( co ) ? co : [ ] ,
} )
} else {
setFormData ( { ... item } )
}
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
setShowModal ( true )
}
const handleDelete = async ( item , type ) => {
const confirmMsg = {
club : ` Verein " ${ item . name } " wirklich löschen? Alle Sparten und Gruppen werden auch gelöscht! ` ,
division : ` Sparte " ${ item . name } " wirklich löschen? ` ,
group : ` Trainingsgruppe " ${ item . name } " wirklich löschen? `
}
if ( ! confirm ( confirmMsg [ type ] ) ) return
try {
if ( type === 'club' ) await api . deleteClub ( item . id )
else if ( type === 'division' ) await api . deleteDivision ( item . id )
else if ( type === 'group' ) await api . deleteTrainingGroup ( item . id )
await loadData ( )
} catch ( err ) {
alert ( 'Fehler beim Löschen: ' + err . message )
}
}
const handleSubmit = async ( e ) => {
e . preventDefault ( )
try {
2026-05-05 16:12:17 +02:00
if ( modalType === 'club' ) {
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
if ( editing ) {
await api . updateClub ( editing . id , formData )
} else {
2026-05-05 16:12:17 +02:00
const payload = {
... formData ,
primary _admin _profile _id : Number ( formData . primary _admin _profile _id ) ,
}
if ( ! payload . primary _admin _profile _id ) {
alert ( 'Hauptverwalter (Profil-ID) ist Pflicht.' )
return
}
await api . createClub ( payload )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
}
} else if ( modalType === 'division' ) {
if ( editing ) {
await api . updateDivision ( editing . id , formData )
} else {
await api . createDivision ( formData )
}
} else if ( modalType === 'group' ) {
2026-05-05 16:40:49 +02:00
const trainerRaw = formData . trainer _id
const trainer _id =
trainerRaw === '' || trainerRaw === undefined || trainerRaw === null
? null
: Number ( trainerRaw )
const coRaw = formData . co _trainer _ids
const co _trainer _ids = Array . isArray ( coRaw )
? coRaw . map ( ( x ) => Number ( x ) ) . filter ( ( n ) => Number . isFinite ( n ) )
: [ ]
const payload = {
... formData ,
trainer _id : Number . isFinite ( trainer _id ) ? trainer _id : null ,
co _trainer _ids ,
}
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
if ( editing ) {
2026-05-05 16:40:49 +02:00
await api . updateTrainingGroup ( editing . id , payload )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
} else {
2026-05-05 16:40:49 +02:00
await api . createTrainingGroup ( payload )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
}
}
setShowModal ( false )
await loadData ( )
} catch ( err ) {
alert ( 'Fehler beim Speichern: ' + err . message )
}
}
const updateFormField = ( field , value ) => {
setFormData ( prev => ( { ... prev , [ field ] : value } ) )
}
2026-05-06 12:49:35 +02:00
const clubTabItems = useMemo ( ( ) => {
const ids = canManageOrgSomewhere
? [ 'clubs' , 'divisions' , 'groups' , 'members' ]
: [ 'clubs' , 'divisions' , 'groups' ]
const labels = {
clubs : 'Vereine' ,
divisions : 'Sparten' ,
groups : 'Trainingsgruppen' ,
members : 'Mitglieder' ,
}
return ids . map ( ( id ) => ( { id , label : labels [ id ] } ) )
} , [ canManageOrgSomewhere ] )
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
if ( loading ) {
return (
2026-05-06 12:20:22 +02:00
< div className = "skills-page__loading" >
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< div className = "spinner" > < / div >
< p > Laden ... < / p >
< / div >
)
}
2026-04-22 06:48:18 +02:00
return (
2026-05-06 12:20:22 +02:00
< div className = "app-page clubs-page" >
< h1 className = "page-title" > Vereinsverwaltung < / h1 >
< p className = "clubs-page__intro muted" >
Für die Trainingsplanung wird mindestens ein < strong > Verein < / strong > und eine < strong > Trainingsgruppe < / strong > gebraucht .
Sparten sind optional — typische Eckdaten einer Gruppe ( Wochentag , Zeit , Ort ) kannst du schrittweise eintragen .
< / p >
2026-05-06 12:49:35 +02:00
< PageSectionNav
ariaLabel = "Vereinsverwaltung"
value = { activeTab }
onChange = { setActiveTab }
items = { clubTabItems }
/ >
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
2026-05-06 12:49:35 +02:00
{ /* Clubs Tab */ }
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
{ activeTab === 'clubs' && (
< >
< div style = { { display : 'flex' , justifyContent : 'space-between' , marginBottom : '1rem' } } >
< h2 > Vereine < / h2 >
2026-04-28 19:46:09 +02:00
{ canCreateClub && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< button className = "btn btn-primary" onClick = { ( ) => handleCreate ( 'club' ) } >
+ Neuer Verein
< / button >
) }
< / div >
{ clubs . length === 0 ? (
< div className = "card" >
2026-04-28 19:46:09 +02:00
< p style = { { color : 'var(--text2)' , textAlign : 'center' , marginBottom : canCreateClub ? '1rem' : 0 } } >
Noch kein Verein angelegt .
{ canCreateClub
? ' Nutze „+ Neuer Verein“ — ein Name reicht zum Start.'
: ' Bitte einen Administrator oder Support um Anlage.' }
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< / p >
2026-04-28 19:46:09 +02:00
{ canCreateClub && (
< p style = { { color : 'var(--text2)' , textAlign : 'center' , fontSize : '0.9rem' } } >
Danach im Tab Trainingsgruppen eine Gruppe diesem Verein zuordnen ; Details sind optional .
< / p >
) }
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< / div >
) : (
< div style = { { display : 'grid' , gap : '1rem' } } >
{ clubs . map ( club => (
< div key = { club . id } className = "card" >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'start' } } >
< div style = { { flex : 1 } } >
< h3 style = { { marginBottom : '0.5rem' } } >
{ club . name }
{ club . abbreviation && (
< span style = { { color : 'var(--text2)' , fontSize : '0.875rem' , marginLeft : '0.5rem' } } >
( { club . abbreviation } )
< / span >
) }
< / h3 >
{ club . description && (
< p style = { { color : 'var(--text2)' , fontSize : '0.875rem' } } >
{ club . description }
< / p >
) }
< span style = { {
display : 'inline-block' ,
marginTop : '0.5rem' ,
fontSize : '0.75rem' ,
padding : '0.25rem 0.5rem' ,
borderRadius : '4px' ,
background : club . status === 'active' ? '#2ea44f' : 'var(--surface2)' ,
color : club . status === 'active' ? 'white' : 'var(--text2)'
} } >
{ club . status }
< / span >
< / div >
2026-05-05 16:12:17 +02:00
{ canManageClub ( club . id ) && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< div style = { { display : 'flex' , gap : '0.5rem' } } >
< button
className = "btn btn-secondary"
onClick = { ( ) => handleEdit ( club , 'club' ) }
>
Bearbeiten
< / button >
2026-05-05 16:12:17 +02:00
{ isSuperAdmin && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< button
className = "btn"
style = { {
background : 'var(--danger)' ,
color : 'white' ,
border : 'none'
} }
onClick = { ( ) => handleDelete ( club , 'club' ) }
>
Löschen
< / button >
2026-05-05 16:12:17 +02:00
) }
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< / div >
) }
< / div >
< / div >
) ) }
< / div >
) }
< / >
) }
{ /* Divisions Tab */ }
{ activeTab === 'divisions' && (
< >
< div style = { { display : 'flex' , justifyContent : 'space-between' , marginBottom : '1rem' } } >
< h2 > Sparten < / h2 >
2026-05-05 16:12:17 +02:00
{ canManageOrgSomewhere && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< button className = "btn btn-primary" onClick = { ( ) => handleCreate ( 'division' ) } >
+ Neue Sparte
< / button >
) }
< / div >
{ divisions . length === 0 ? (
< div className = "card" >
< p style = { { color : 'var(--text2)' , textAlign : 'center' } } >
Keine Sparten gefunden
< / p >
< / div >
) : (
< div style = { { display : 'grid' , gap : '1rem' } } >
{ divisions . map ( division => (
< div key = { division . id } className = "card" >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'start' } } >
< div style = { { flex : 1 } } >
< h3 style = { { marginBottom : '0.5rem' } } > { division . name } < / h3 >
< p style = { { color : 'var(--text2)' , fontSize : '0.875rem' } } >
Verein : { division . club _name }
< / p >
{ division . focus _area && (
< span style = { {
display : 'inline-block' ,
marginTop : '0.5rem' ,
fontSize : '0.75rem' ,
padding : '0.25rem 0.5rem' ,
borderRadius : '4px' ,
background : 'var(--surface2)' ,
color : 'var(--text2)'
} } >
{ division . focus _area }
< / span >
) }
< / div >
2026-05-05 16:12:17 +02:00
{ canManageClub ( division . club _id ) && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< div style = { { display : 'flex' , gap : '0.5rem' } } >
< button
className = "btn btn-secondary"
onClick = { ( ) => handleEdit ( division , 'division' ) }
>
Bearbeiten
< / button >
< button
className = "btn"
style = { {
background : 'var(--danger)' ,
color : 'white' ,
border : 'none'
} }
onClick = { ( ) => handleDelete ( division , 'division' ) }
>
Löschen
< / button >
< / div >
) }
< / div >
< / div >
) ) }
< / div >
) }
< / >
) }
{ /* Training Groups Tab */ }
{ activeTab === 'groups' && (
< >
< div style = { { display : 'flex' , justifyContent : 'space-between' , marginBottom : '1rem' } } >
< h2 > Trainingsgruppen < / h2 >
2026-05-05 16:12:17 +02:00
{ canCreateTrainingGroup && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< button className = "btn btn-primary" onClick = { ( ) => handleCreate ( 'group' ) } >
+ Neue Gruppe
< / button >
) }
< / div >
{ groups . length === 0 ? (
< div className = "card" >
< p style = { { color : 'var(--text2)' , textAlign : 'center' } } >
Keine Trainingsgruppen gefunden
< / p >
< / div >
) : (
2026-05-06 12:41:04 +02:00
< div
className = "card-grid clubs-groups-card-grid"
style = { {
gridTemplateColumns : 'repeat(auto-fill, minmax(320px, 1fr))' ,
gap : '1rem'
} }
>
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
{ groups . map ( group => (
< div key = { group . id } className = "card" >
< h3 style = { { marginBottom : '0.5rem' } } > { group . name } < / h3 >
< p style = { { color : 'var(--text2)' , fontSize : '0.875rem' , marginBottom : '0.5rem' } } >
{ group . club _name }
{ group . division _name && ` · ${ group . division _name } ` }
< / p >
< div style = { { fontSize : '0.875rem' , color : 'var(--text2)' , marginBottom : '1rem' } } >
{ group . weekday && group . time _start && (
< div > 📅 { group . weekday } , { group . time _start . slice ( 0 , 5 ) } - { group . time _end ? . slice ( 0 , 5 ) } < / div >
) }
{ group . location && < div > 📍 { group . location } < / div > }
{ group . trainer _name && < div > 👤 { group . trainer _name } < / div > }
{ group . level && < div > ⭐ { group . level } < / div > }
{ group . age _group && < div > 👶 { group . age _group } < / div > }
< / div >
2026-05-05 16:12:17 +02:00
{ canEditGroup ( group ) && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< div style = { { display : 'flex' , gap : '0.5rem' , marginTop : 'auto' } } >
< button
className = "btn btn-secondary"
style = { { flex : 1 } }
onClick = { ( ) => handleEdit ( group , 'group' ) }
>
Bearbeiten
< / button >
2026-05-05 16:12:17 +02:00
{ canDeleteGroup ( group ) && (
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< button
className = "btn"
style = { {
background : 'var(--danger)' ,
color : 'white' ,
border : 'none'
} }
onClick = { ( ) => handleDelete ( group , 'group' ) }
>
Löschen
< / button >
) }
< / div >
) }
< / div >
) ) }
< / div >
) }
< / >
) }
2026-05-05 16:40:49 +02:00
{ activeTab === 'members' && canManageOrgSomewhere && (
< >
< div style = { { display : 'flex' , justifyContent : 'space-between' , marginBottom : '1rem' , flexWrap : 'wrap' , gap : '0.75rem' } } >
< h2 > Mitglieder & amp ; Beitrittsanträge < / h2 >
< div style = { { display : 'flex' , gap : '0.5rem' , alignItems : 'center' , flexWrap : 'wrap' } } >
< label style = { { color : 'var(--text2)' , fontSize : '0.875rem' } } > Verein < / label >
< select
className = "form-input"
style = { { minWidth : '200px' } }
value = { membersAdminClubId ? ? '' }
onChange = { ( e ) =>
setMembersAdminClubId ( e . target . value ? parseInt ( e . target . value , 10 ) : null )
}
>
{ manageableClubs . map ( ( c ) => (
< option key = { c . id } value = { c . id } >
{ c . name }
< / option >
) ) }
< / select >
< button
type = "button"
className = "btn btn-primary"
onClick = { ( ) => {
setAddMemberForm ( { profile _id : '' , roles : [ 'trainer' ] } )
setShowAddMemberModal ( true )
} }
>
Mitglied hinzufügen
< / button >
< / div >
< / div >
{ manageableClubs . length === 0 ? (
< div className = "card" >
< p style = { { color : 'var(--text2)' } } >
Keine Vereine , für die du Mitglieder verwalten darfst .
< / p >
< / div >
) : membersAdminLoading ? (
< p style = { { color : 'var(--text2)' } } > Laden … < / p >
) : (
< >
{ joinRequestsAdmin . length > 0 && (
< div className = "card" style = { { marginBottom : '1rem' } } >
< h3 style = { { marginTop : 0 } } > Offene Beitrittsanträge < / h3 >
< div style = { { display : 'grid' , gap : '0.75rem' } } >
{ joinRequestsAdmin . map ( ( req ) => (
< div
key = { req . id }
style = { {
padding : '0.75rem' ,
borderRadius : '8px' ,
border : '1px solid var(--border)' ,
display : 'flex' ,
justifyContent : 'space-between' ,
flexWrap : 'wrap' ,
gap : '0.5rem' ,
} }
>
< div >
< strong > { req . applicant _name || req . applicant _email || 'Nutzer' } < / strong >
< div style = { { fontSize : '0.85rem' , color : 'var(--text2)' } } >
{ req . applicant _email } · Profil # { req . profile _id }
< / div >
{ req . message ? (
< div style = { { marginTop : '0.35rem' , fontSize : '0.875rem' } } > { req . message } < / div >
) : null }
< / div >
< div style = { { display : 'flex' , gap : '0.35rem' , flexWrap : 'wrap' } } >
< button
type = "button"
className = "btn btn-primary"
onClick = { ( ) =>
setAcceptJoinModal ( {
id : req . id ,
label : req . applicant _name || req . applicant _email ,
roles : [ 'trainer' ] ,
} )
}
>
Annehmen
< / button >
< button
type = "button"
className = "btn btn-secondary"
onClick = { async ( ) => {
if ( ! confirm ( 'Antrag ablehnen?' ) ) return
try {
await api . rejectClubJoinRequest ( membersAdminClubId , req . id )
2026-05-09 09:13:38 +02:00
notifyOrgInboxChanged ( )
2026-05-05 16:40:49 +02:00
await reloadMembersAdmin ( )
} catch ( err ) {
alert ( err . message || String ( err ) )
}
} }
>
Ablehnen
< / button >
< / div >
< / div >
) ) }
< / div >
< / div >
) }
< div className = "card" >
< h3 style = { { marginTop : 0 } } > Mitglieder < / h3 >
2026-05-09 10:55:58 +02:00
{ isPlatformAdmin ? (
< p
className = "muted"
style = { { fontSize : '0.85rem' , marginTop : '-0.35rem' , marginBottom : '0.85rem' , lineHeight : 1.45 } }
>
Liste enthält < strong style = { { color : 'var(--text1)' } } > aktive und deaktivierte < / strong > Vereinszugänge .
Deaktiviert gilt pro Verein ( ohne Kontosperre ) — auch für Super - Admins ohne aktive Mitgliedschaft in diesem
Verein kein Zugriff auf dessen Vereinsinhalte . Wiederherstellen über die Schaltflächen oder Mitglied
bearbeiten .
< / p >
) : (
< p
className = "muted"
style = { { fontSize : '0.85rem' , marginTop : '-0.35rem' , marginBottom : '0.85rem' , lineHeight : 1.45 } }
>
Deaktivierte Vereinszugänge sind hervorgehoben — { ' ' }
< strong > Anmeldung < / strong > bleibt möglich , < strong > Vereinsinhalte < / strong > dieser Zuordnung nicht .
< / p >
) }
2026-05-05 16:40:49 +02:00
{ clubMembersAdmin . length === 0 ? (
< p style = { { color : 'var(--text2)' } } > Noch keine Mitglieder erfasst . < / p >
) : (
< div style = { { display : 'grid' , gap : '0.65rem' } } >
2026-05-09 10:55:58 +02:00
{ clubMembersAdmin . map ( ( m ) => {
const memStatus = ( m . status || 'active' ) . toLowerCase ( )
const inactiveRow = memStatus === 'inactive'
const portalLabel = ( m . portal _role || '' ) . trim ( )
return (
2026-05-05 16:40:49 +02:00
< div
key = { m . membership _id }
style = { {
padding : '0.65rem' ,
borderRadius : '8px' ,
2026-05-09 10:55:58 +02:00
background : inactiveRow ? 'color-mix(in srgb, var(--warning, #884400) 12%, var(--surface2))' : 'var(--surface2)' ,
border : inactiveRow ? '1px solid color-mix(in srgb, var(--warning, #d4a012) 40%, transparent)' : undefined ,
2026-05-05 16:40:49 +02:00
display : 'flex' ,
justifyContent : 'space-between' ,
flexWrap : 'wrap' ,
gap : '0.5rem' ,
} }
>
< div >
< strong > { m . name || m . email } < / strong >
< div style = { { fontSize : '0.8rem' , color : 'var(--text2)' } } >
2026-05-09 10:55:58 +02:00
{ m . email } · # { m . profile _id }
{ ' · ' }
< span style = { { fontWeight : 600 } } >
Vereinszugang : { ' ' }
< span style = { { color : inactiveRow ? 'var(--warning, #d4a012)' : 'inherit' } } >
{ inactiveRow ? 'deaktiviert' : 'aktiv' }
< / span >
< / span >
{ portalLabel ? (
< span style = { { color : 'var(--text3)' , marginLeft : '0.35rem' } } > · Portal : { portalLabel } < / span >
) : null }
2026-05-05 16:40:49 +02:00
< / div >
< div style = { { fontSize : '0.8rem' , marginTop : '0.25rem' } } >
Rollen : { ( m . roles || [ ] ) . join ( ', ' ) || '—' }
< / div >
< / div >
2026-05-09 10:55:58 +02:00
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.35rem' , alignItems : 'stretch' } } >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setEditMemberModal ( m ) } >
Mitglied bearbeiten
< / button >
2026-05-09 13:26:22 +02:00
{ m . profile _id !== user ? . id ? (
< button
type = "button"
className = "btn btn-secondary"
disabled = {
! isSuperAdmin &&
! isPlatformAdmin &&
isEscalatedPortalRole ( m . portal _role )
}
title = {
! isSuperAdmin &&
! isPlatformAdmin &&
isEscalatedPortalRole ( m . portal _role )
? 'Für Portal-Administratoren nur mit Super-/Plattform-Administrator möglich'
: undefined
}
onClick = { ( ) => {
setPwdNew ( '' )
setPwdNew2 ( '' )
setPwdModal ( {
profileId : m . profile _id ,
label : m . name || m . email || ` # ${ m . profile _id } ` ,
} )
} }
>
Passwort / Link
< / button >
) : null }
2026-05-09 10:55:58 +02:00
{ m . profile _id !== user ? . id ? (
inactiveRow ? (
< button
type = "button"
className = "btn btn-primary"
onClick = { ( ) => toggleMembersAdminClubAccess ( m , true ) }
>
Vereinszugang aktivieren
< / button >
) : (
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) => toggleMembersAdminClubAccess ( m , false ) }
>
Vereinszugang deaktivieren
< / button >
)
) : null }
< / div >
2026-05-05 16:40:49 +02:00
< / div >
2026-05-09 10:55:58 +02:00
)
} ) }
2026-05-05 16:40:49 +02:00
< / div >
) }
< / div >
< / >
) }
< / >
) }
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
{ /* Modal */ }
{ showModal && (
< div style = { {
position : 'fixed' ,
top : 0 ,
left : 0 ,
right : 0 ,
bottom : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1000 ,
padding : '1rem'
} } >
< div style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '2rem' ,
maxWidth : '600px' ,
width : '100%' ,
maxHeight : '90vh' ,
overflowY : 'auto'
} } >
< h2 style = { { marginBottom : '1.5rem' } } >
{ editing
? ( modalType === 'club' ? 'Verein bearbeiten' : modalType === 'division' ? 'Sparte bearbeiten' : 'Gruppe bearbeiten' )
: ( modalType === 'club' ? 'Neuer Verein' : modalType === 'division' ? 'Neue Sparte' : 'Neue Gruppe' )
}
< / h2 >
< form onSubmit = { handleSubmit } >
{ /* Club Form */ }
{ modalType === 'club' && (
< >
< div className = "form-row" >
< label className = "form-label" > Name * < / label >
< input
type = "text"
className = "form-input"
value = { formData . name || '' }
onChange = { ( e ) => updateFormField ( 'name' , e . target . value ) }
required
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Kürzel < / label >
< input
type = "text"
className = "form-input"
value = { formData . abbreviation || '' }
onChange = { ( e ) => updateFormField ( 'abbreviation' , e . target . value ) }
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Beschreibung < / label >
< textarea
className = "form-input"
rows = { 3 }
value = { formData . description || '' }
onChange = { ( e ) => updateFormField ( 'description' , e . target . value ) }
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Status < / label >
< select
className = "form-input"
value = { formData . status || 'active' }
onChange = { ( e ) => updateFormField ( 'status' , e . target . value ) }
>
< option value = "active" > Aktiv < / option >
< option value = "inactive" > Inaktiv < / option >
< / select >
< / div >
2026-05-05 16:12:17 +02:00
{ ! editing && canCreateClub && (
< div className = "form-row" >
< label className = "form-label" > Hauptverwalter ( Profil - ID ) * < / label >
< input
type = "number"
min = { 1 }
className = "form-input"
value = { formData . primary _admin _profile _id ? ? '' }
onChange = { ( e ) =>
updateFormField (
'primary_admin_profile_id' ,
e . target . value === '' ? '' : parseInt ( e . target . value , 10 )
)
}
required
/ >
< p style = { { fontSize : '0.8rem' , color : 'var(--text3)' , marginTop : '0.35rem' } } >
Nur Plattform - Administratoren legen Vereine an . Standard ist deine eigene Profil - ID .
< / p >
< / div >
) }
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< / >
) }
{ /* Division Form */ }
{ modalType === 'division' && (
< >
< div className = "form-row" >
< label className = "form-label" > Verein * < / label >
< select
className = "form-input"
value = { formData . club _id || '' }
onChange = { ( e ) => updateFormField ( 'club_id' , parseInt ( e . target . value ) ) }
required
disabled = { editing }
>
< option value = "" > Bitte wählen < / option >
{ clubs . map ( club => (
< option key = { club . id } value = { club . id } > { club . name } < / option >
) ) }
< / select >
< / div >
< div className = "form-row" >
< label className = "form-label" > Name * < / label >
< input
type = "text"
className = "form-input"
value = { formData . name || '' }
onChange = { ( e ) => updateFormField ( 'name' , e . target . value ) }
required
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Fokusbereich < / label >
< select
className = "form-input"
value = { formData . focus _area || '' }
onChange = { ( e ) => updateFormField ( 'focus_area' , e . target . value ) }
>
< option value = "" > Bitte wählen < / option >
< option value = "karate" > Karate < / option >
< option value = "selbstverteidigung" > Selbstverteidigung < / option >
< option value = "gewaltschutz" > Gewaltschutz < / option >
< / select >
< / div >
< / >
) }
{ /* Group Form */ }
{ modalType === 'group' && (
< >
< div className = "form-row" >
< label className = "form-label" > Verein * < / label >
< select
className = "form-input"
value = { formData . club _id || '' }
onChange = { ( e ) => updateFormField ( 'club_id' , parseInt ( e . target . value ) ) }
required
disabled = { editing }
>
< option value = "" > Bitte wählen < / option >
{ clubs . map ( club => (
< option key = { club . id } value = { club . id } > { club . name } < / option >
) ) }
< / select >
< / div >
< div className = "form-row" >
< label className = "form-label" > Sparte ( optional ) < / label >
< select
className = "form-input"
value = { formData . division _id || '' }
onChange = { ( e ) => updateFormField ( 'division_id' , e . target . value ? parseInt ( e . target . value ) : null ) }
>
< option value = "" > Keine Sparte < / option >
{ divisions . filter ( d => d . club _id === formData . club _id ) . map ( div => (
< option key = { div . id } value = { div . id } > { div . name } < / option >
) ) }
< / select >
< / div >
< div className = "form-row" >
< label className = "form-label" > Name * < / label >
< input
type = "text"
className = "form-input"
value = { formData . name || '' }
onChange = { ( e ) => updateFormField ( 'name' , e . target . value ) }
required
/ >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : '1rem' } } >
< div className = "form-row" >
< label className = "form-label" > Level < / label >
< input
type = "text"
className = "form-input"
value = { formData . level || '' }
onChange = { ( e ) => updateFormField ( 'level' , e . target . value ) }
placeholder = "z.B. Anfänger, Fortgeschritten"
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Altersgruppe < / label >
< input
type = "text"
className = "form-input"
value = { formData . age _group || '' }
onChange = { ( e ) => updateFormField ( 'age_group' , e . target . value ) }
placeholder = "z.B. Kinder, Erwachsene"
/ >
< / div >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr 1fr' , gap : '1rem' } } >
< div className = "form-row" >
< label className = "form-label" > Wochentag < / label >
< select
className = "form-input"
value = { formData . weekday || '' }
onChange = { ( e ) => updateFormField ( 'weekday' , e . target . value ) }
>
< option value = "" > - < / option >
< option value = "Montag" > Montag < / option >
< option value = "Dienstag" > Dienstag < / option >
< option value = "Mittwoch" > Mittwoch < / option >
< option value = "Donnerstag" > Donnerstag < / option >
< option value = "Freitag" > Freitag < / option >
< option value = "Samstag" > Samstag < / option >
< option value = "Sonntag" > Sonntag < / option >
< / select >
< / div >
< div className = "form-row" >
< label className = "form-label" > Von < / label >
< input
type = "time"
className = "form-input"
value = { formData . time _start || '' }
onChange = { ( e ) => updateFormField ( 'time_start' , e . target . value ) }
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Bis < / label >
< input
type = "time"
className = "form-input"
value = { formData . time _end || '' }
onChange = { ( e ) => updateFormField ( 'time_end' , e . target . value ) }
/ >
< / div >
< / div >
< div className = "form-row" >
< label className = "form-label" > Ort < / label >
< input
type = "text"
className = "form-input"
value = { formData . location || '' }
onChange = { ( e ) => updateFormField ( 'location' , e . target . value ) }
placeholder = "z.B. Sporthalle Musterstadt"
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > Fokus < / label >
< input
type = "text"
className = "form-input"
value = { formData . focus || '' }
onChange = { ( e ) => updateFormField ( 'focus' , e . target . value ) }
placeholder = "z.B. Kata, Kumite"
/ >
< / div >
2026-05-05 16:40:49 +02:00
< div className = "form-row" >
< label className = "form-label" > Haupttrainer < / label >
< select
className = "form-input"
value = { formData . trainer _id != null ? String ( formData . trainer _id ) : '' }
onChange = { ( e ) =>
updateFormField (
'trainer_id' ,
e . target . value === '' ? '' : parseInt ( e . target . value , 10 )
)
}
>
< option value = "" > — < / option >
{ groupMemberDirectory . map ( ( p ) => (
< option key = { p . id } value = { p . id } >
{ ( p . name || p . email || '' ) . trim ( ) } ( # { p . id } )
< / option >
) ) }
< / select >
< p style = { { fontSize : '0.75rem' , color : 'var(--text3)' , marginTop : '0.35rem' } } >
Liste = aktive Vereinsmitglieder . Nach Zuweisungen ggf . Seite neu laden .
< / p >
< / div >
< div className = "form-row" >
< label className = "form-label" > Co - Trainer ( Mehrfachauswahl ) < / label >
< select
multiple
className = "form-input"
size = { Math . min ( 8 , Math . max ( 4 , groupMemberDirectory . length || 4 ) ) }
value = { ( formData . co _trainer _ids || [ ] ) . map ( String ) }
onChange = { ( e ) => {
const opts = [ ... e . target . selectedOptions ] . map ( ( o ) => parseInt ( o . value , 10 ) )
updateFormField ( 'co_trainer_ids' , opts )
} }
>
{ groupMemberDirectory . map ( ( p ) => (
< option key = { p . id } value = { p . id } >
{ ( p . name || p . email || '' ) . trim ( ) } ( # { p . id } )
< / option >
) ) }
< / select >
< / div >
feat: Clubs & Organization Management complete
Backend:
- Created routers/clubs.py with full CRUD
- Clubs: list, get, create, update, delete (admin only)
- Divisions: list, create, update, delete (admin only)
- Training Groups: list, get, create, update, delete (admin/trainer)
- Registered clubs router in main.py
- Permission checks: admin for clubs/divisions, trainer for groups
Frontend:
- Complete ClubsPage with 3 tabs (Vereine, Sparten, Gruppen)
- Role-based UI (admin sees all actions, trainer can manage groups)
- Full CRUD forms with modals
- Mobile-responsive card layouts
- Updated api.js with all club/division/group functions
Migration already exists: 002_organization.sql (clubs, divisions, training_groups)
Next: Skills & Methods display (read-only)
2026-04-22 16:48:02 +02:00
< div className = "form-row" >
< label className = "form-label" > Status < / label >
< select
className = "form-input"
value = { formData . status || 'active' }
onChange = { ( e ) => updateFormField ( 'status' , e . target . value ) }
>
< option value = "active" > Aktiv < / option >
< option value = "inactive" > Inaktiv < / option >
< / select >
< / div >
< / >
) }
{ /* Buttons */ }
< div style = { { display : 'flex' , gap : '0.5rem' , marginTop : '1.5rem' } } >
< button type = "submit" className = "btn btn-primary" style = { { flex : 1 } } >
{ editing ? 'Speichern' : 'Erstellen' }
< / button >
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) => setShowModal ( false ) }
>
Abbrechen
< / button >
< / div >
< / form >
< / div >
< / div >
) }
2026-05-05 16:40:49 +02:00
{ showAddMemberModal && membersAdminClubId && (
< div
style = { {
position : 'fixed' ,
top : 0 ,
left : 0 ,
right : 0 ,
bottom : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1100 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '480px' ,
width : '100%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Mitglied hinzufügen < / h2 >
< p style = { { color : 'var(--text2)' , fontSize : '0.875rem' } } >
{ isPlatformAdmin
? 'Nutzer aus der Liste wählen oder Profil-ID eingeben.'
: 'Profil-ID des Nutzers (z. B. aus der Nutzerverwaltung). Offene Beitrittsanträge kannst du oben direkt annehmen.' }
< / p >
{ isPlatformAdmin && profilesAdminList . length > 0 ? (
< div className = "form-row" >
< label className = "form-label" > Profil < / label >
< select
className = "form-input"
value = { addMemberForm . profile _id === '' ? '' : String ( addMemberForm . profile _id ) }
onChange = { ( e ) =>
setAddMemberForm ( ( prev ) => ( { ... prev , profile _id : e . target . value } ) )
}
>
< option value = "" > Bitte wählen … < / option >
{ profilesAdminList . map ( ( p ) => (
< option key = { p . id } value = { p . id } >
# { p . id } — { p . email || '—' } ( { p . name || 'ohne Name' } )
< / option >
) ) }
< / select >
< / div >
) : (
< div className = "form-row" >
< label className = "form-label" > Profil - ID < / label >
< input
type = "number"
min = { 1 }
className = "form-input"
value = { addMemberForm . profile _id }
onChange = { ( e ) =>
setAddMemberForm ( ( prev ) => ( {
... prev ,
profile _id : e . target . value === '' ? '' : parseInt ( e . target . value , 10 ) ,
} ) )
}
/ >
< / 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 = { addMemberForm . roles . includes ( opt . code ) }
onChange = { ( ) => {
setAddMemberForm ( ( prev ) => {
const set = new Set ( prev . roles )
if ( set . has ( opt . code ) ) set . delete ( opt . code )
else set . add ( opt . code )
return { ... prev , roles : Array . from ( set ) }
} )
} }
/ >
{ 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 = { async ( ) => {
const raw = addMemberForm . profile _id
const profile _id = typeof raw === 'number' ? raw : parseInt ( String ( raw ) , 10 )
if ( ! Number . isFinite ( profile _id ) || profile _id < 1 ) {
alert ( 'Gültige Profil-ID wählen.' )
return
}
if ( ! addMemberForm . roles . length ) {
alert ( 'Mindestens eine Rolle.' )
return
}
try {
await api . addClubMember ( membersAdminClubId , {
profile _id ,
roles : addMemberForm . roles ,
} )
setShowAddMemberModal ( false )
await reloadMembersAdmin ( )
await loadData ( )
} catch ( err ) {
alert ( err . message || String ( err ) )
}
} }
>
Hinzufügen
< / button >
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) => setShowAddMemberModal ( false ) }
>
Abbrechen
< / button >
< / div >
< / div >
< / div >
) }
{ acceptJoinModal && membersAdminClubId && (
< div
style = { {
position : 'fixed' ,
top : 0 ,
left : 0 ,
right : 0 ,
bottom : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1100 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '480px' ,
width : '100%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Antrag annehmen < / h2 >
< p style = { { color : 'var(--text2)' } } > { acceptJoinModal . label } < / p >
< div className = "form-row" >
< span className = "form-label" > Rollen bei Aufnahme < / 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 = { acceptJoinModal . roles . includes ( opt . code ) }
onChange = { ( ) => {
setAcceptJoinModal ( ( prev ) => {
if ( ! prev ) return prev
const set = new Set ( prev . roles )
if ( set . has ( opt . code ) ) set . delete ( opt . code )
else set . add ( opt . code )
const roles = Array . from ( set )
return { ... prev , roles : roles . length ? roles : [ '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 = { async ( ) => {
try {
await api . acceptClubJoinRequest (
membersAdminClubId ,
acceptJoinModal . id ,
acceptJoinModal . roles . length ? acceptJoinModal . roles : [ 'trainer' ]
)
2026-05-09 09:13:38 +02:00
notifyOrgInboxChanged ( )
2026-05-05 16:40:49 +02:00
setAcceptJoinModal ( null )
await reloadMembersAdmin ( )
await loadData ( )
} catch ( err ) {
alert ( err . message || String ( err ) )
}
} }
>
Aufnehmen
< / button >
< button
type = "button"
className = "btn btn-secondary"
onClick = { ( ) => setAcceptJoinModal ( null ) }
>
Abbrechen
< / button >
< / div >
< / div >
< / div >
) }
{ editMemberModal && membersAdminClubId && (
< div
style = { {
position : 'fixed' ,
top : 0 ,
left : 0 ,
right : 0 ,
bottom : 0 ,
background : 'rgba(0,0,0,0.5)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
zIndex : 1100 ,
padding : '1rem' ,
} }
>
< div
style = { {
background : 'var(--surface)' ,
borderRadius : '12px' ,
padding : '1.5rem' ,
maxWidth : '480px' ,
width : '100%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Mitglied bearbeiten < / h2 >
< p style = { { color : 'var(--text2)' , fontSize : '0.9rem' } } >
{ editMemberModal . name || editMemberModal . email } ( # { editMemberModal . profile _id } )
< / p >
< div className = "form-row" >
2026-05-09 10:42:56 +02:00
< label className = "form-label" > Vereinszugang < / label >
2026-05-05 16:40:49 +02:00
< select
className = "form-input"
value = { editMemberModal . status || 'active' }
onChange = { ( e ) =>
setEditMemberModal ( ( 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 — weiter anmeldbar , keine Vereinsinhalte < / option >
2026-05-05 16:40:49 +02:00
< / select >
< / div >
2026-05-09 10:42:56 +02:00
< p className = "muted" style = { { fontSize : '0.82rem' , lineHeight : 1.45 } } >
Deaktivierung gilt nur für diesen Verein ; der Login - Account bleibt aktiv .
< / p >
2026-05-05 16:40:49 +02:00
< 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 = { ( editMemberModal . roles || [ ] ) . includes ( opt . code ) }
onChange = { ( ) => {
setEditMemberModal ( ( prev ) => {
if ( ! prev ) return prev
const set = new Set ( prev . roles || [ ] )
if ( set . has ( opt . code ) ) set . delete ( opt . code )
else set . add ( opt . code )
let roles = Array . from ( set )
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 = { async ( ) => {
try {
await api . updateClubMember ( membersAdminClubId , editMemberModal . profile _id , {
roles : editMemberModal . roles ,
status : editMemberModal . status ,
} )
setEditMemberModal ( null )
await reloadMembersAdmin ( )
await loadData ( )
} catch ( err ) {
alert ( err . message || String ( err ) )
}
} }
>
Speichern
< / button >
< button
type = "button"
className = "btn"
style = { { background : 'var(--danger)' , color : '#fff' , border : 'none' } }
onClick = { async ( ) => {
if (
! confirm (
'Mitgliedschaft wirklich entfernen? (Nutzer verliert alle Rollen in diesem Verein.)'
)
)
return
try {
await api . removeClubMember ( membersAdminClubId , editMemberModal . profile _id )
setEditMemberModal ( null )
await reloadMembersAdmin ( )
await loadData ( )
} catch ( err ) {
alert ( err . message || String ( err ) )
}
} }
>
Aus Verein entfernen
< / button >
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setEditMemberModal ( null ) } >
Schließen
< / button >
< / div >
< / div >
< / div >
) }
2026-05-09 13:26:22 +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%' ,
} }
>
< h2 style = { { marginTop : 0 } } > Passwort zurücksetzen < / h2 >
< p style = { { color : 'var(--text2)' , fontSize : '0.9rem' } } > { pwdModal . label } < / p >
< p className = "muted" style = { { fontSize : '0.82rem' , marginBottom : '0.75rem' } } >
Standard : Es wird ein sicherer Link per E - Mail verschickt ( wie „ Passwort vergessen “ ) . Das bisherige Passwort
2026-10-13 16:47:57 +02:00
bleibt gültig , bis die Person den Link nutzt und ein neues Passwort wählt . Zwei - Faktor bleibt dabei aktiv .
2026-05-09 13:26:22 +02:00
< / p >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.5rem' } } >
< button type = "button" className = "btn btn-primary" onClick = { submitMembersPasswordEmail } >
Reset - Link per E - Mail senden
< / button >
< / div >
{ isSuperAdmin ? (
< >
< 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 = { submitMembersPasswordDirect }
>
Passwort direkt setzen
< / button >
< / >
) : null }
< div style = { { marginTop : '1rem' } } >
< button
type = "button"
className = "btn btn-secondary"
style = { { width : '100%' } }
onClick = { ( ) => {
setPwdModal ( null )
setPwdNew ( '' )
setPwdNew2 ( '' )
} }
>
Schließen
< / button >
< / div >
< / div >
< / div >
) : null }
2026-04-22 06:48:18 +02:00
< / div >
)
}
export default ClubsPage