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 React, { useState, useEffect } from 'react'
|
|
|
|
|
import api from '../utils/api'
|
|
|
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
|
|
// Form state
|
|
|
|
|
const [formData, setFormData] = useState({})
|
|
|
|
|
|
|
|
|
|
const isAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
|
|
|
|
const isTrainer = user?.role === 'trainer' || isAdmin
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCreate = (type) => {
|
|
|
|
|
setEditing(null)
|
|
|
|
|
setModalType(type)
|
|
|
|
|
|
|
|
|
|
if (type === 'club') {
|
|
|
|
|
setFormData({ name: '', abbreviation: '', description: '', status: 'active' })
|
|
|
|
|
} 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: '',
|
|
|
|
|
trainer_id: '',
|
|
|
|
|
co_trainer_ids: [],
|
|
|
|
|
status: 'active'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEdit = (item, type) => {
|
|
|
|
|
setEditing(item)
|
|
|
|
|
setModalType(type)
|
|
|
|
|
setFormData({ ...item })
|
|
|
|
|
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 {
|
|
|
|
|
if (modalType === 'club') {
|
|
|
|
|
if (editing) {
|
|
|
|
|
await api.updateClub(editing.id, formData)
|
|
|
|
|
} else {
|
|
|
|
|
await api.createClub(formData)
|
|
|
|
|
}
|
|
|
|
|
} else if (modalType === 'division') {
|
|
|
|
|
if (editing) {
|
|
|
|
|
await api.updateDivision(editing.id, formData)
|
|
|
|
|
} else {
|
|
|
|
|
await api.createDivision(formData)
|
|
|
|
|
}
|
|
|
|
|
} else if (modalType === 'group') {
|
|
|
|
|
if (editing) {
|
|
|
|
|
await api.updateTrainingGroup(editing.id, formData)
|
|
|
|
|
} else {
|
|
|
|
|
await api.createTrainingGroup(formData)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setShowModal(false)
|
|
|
|
|
await loadData()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert('Fehler beim Speichern: ' + err.message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateFormField = (field, value) => {
|
|
|
|
|
setFormData(prev => ({ ...prev, [field]: value }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
|
|
|
<div className="spinner"></div>
|
|
|
|
|
<p>Laden...</p>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 06:48:18 +02:00
|
|
|
return (
|
|
|
|
|
<div style={{ padding: '2rem' }}>
|
|
|
|
|
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
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
|
|
|
<h1 style={{ marginBottom: '1.5rem' }}>Vereinsverwaltung</h1>
|
2026-04-22 06:48:18 +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
|
|
|
{/* Tabs */}
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
gap: '0.5rem',
|
|
|
|
|
marginBottom: '1.5rem',
|
|
|
|
|
borderBottom: '2px solid var(--border)'
|
|
|
|
|
}}>
|
|
|
|
|
{['clubs', 'divisions', 'groups'].map(tab => (
|
|
|
|
|
<button
|
|
|
|
|
key={tab}
|
|
|
|
|
onClick={() => setActiveTab(tab)}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '0.75rem 1.5rem',
|
|
|
|
|
background: activeTab === tab ? 'var(--accent)' : 'transparent',
|
|
|
|
|
color: activeTab === tab ? 'white' : 'var(--text1)',
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '8px 8px 0 0',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
fontWeight: activeTab === tab ? 'bold' : 'normal'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tab === 'clubs' && 'Vereine'}
|
|
|
|
|
{tab === 'divisions' && 'Sparten'}
|
|
|
|
|
{tab === 'groups' && 'Trainingsgruppen'}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-04-22 06:48:18 +02:00
|
|
|
</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
|
|
|
|
|
|
|
|
{/* Clubs Tab */}
|
|
|
|
|
{activeTab === 'clubs' && (
|
|
|
|
|
<>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
|
|
|
|
<h2>Vereine</h2>
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<button className="btn btn-primary" onClick={() => handleCreate('club')}>
|
|
|
|
|
+ Neuer Verein
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{clubs.length === 0 ? (
|
|
|
|
|
<div className="card">
|
|
|
|
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
|
|
|
|
Keine Vereine gefunden
|
|
|
|
|
</p>
|
|
|
|
|
</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>
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
onClick={() => handleEdit(club, 'club')}
|
|
|
|
|
>
|
|
|
|
|
Bearbeiten
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="btn"
|
|
|
|
|
style={{
|
|
|
|
|
background: 'var(--danger)',
|
|
|
|
|
color: 'white',
|
|
|
|
|
border: 'none'
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => handleDelete(club, 'club')}
|
|
|
|
|
>
|
|
|
|
|
Löschen
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Divisions Tab */}
|
|
|
|
|
{activeTab === 'divisions' && (
|
|
|
|
|
<>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
|
|
|
|
<h2>Sparten</h2>
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<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>
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<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>
|
|
|
|
|
{isTrainer && (
|
|
|
|
|
<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>
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
|
|
|
|
gap: '1rem'
|
|
|
|
|
}}>
|
|
|
|
|
{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>
|
|
|
|
|
|
|
|
|
|
{(isAdmin || group.trainer_id === user?.id) && (
|
|
|
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
onClick={() => handleEdit(group, 'group')}
|
|
|
|
|
>
|
|
|
|
|
Bearbeiten
|
|
|
|
|
</button>
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<button
|
|
|
|
|
className="btn"
|
|
|
|
|
style={{
|
|
|
|
|
background: 'var(--danger)',
|
|
|
|
|
color: 'white',
|
|
|
|
|
border: 'none'
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => handleDelete(group, 'group')}
|
|
|
|
|
>
|
|
|
|
|
Löschen
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 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>
|
|
|
|
|
|
|
|
|
|
<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-04-22 06:48:18 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ClubsPage
|