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)
This commit is contained in:
parent
8c7cf91cef
commit
8e027e02bb
|
|
@ -70,15 +70,16 @@ def read_root():
|
|||
}
|
||||
|
||||
# Register routers
|
||||
from routers import auth, profiles, exercises
|
||||
from routers import auth, profiles, exercises, clubs
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(profiles.router)
|
||||
app.include_router(exercises.router)
|
||||
app.include_router(clubs.router)
|
||||
|
||||
# TODO: Add more routers as they are created
|
||||
# from routers import clubs, groups, skills, methods
|
||||
# app.include_router(clubs.router, prefix="/api")
|
||||
# from routers import skills, methods
|
||||
# app.include_router(skills.router, prefix="/api")
|
||||
# ... etc
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
528
backend/routers/clubs.py
Normal file
528
backend/routers/clubs.py
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
"""
|
||||
Club & Organization Management Endpoints for Shinkan Jinkendo
|
||||
|
||||
Handles CRUD operations for clubs, divisions, and training groups.
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
|
||||
from db import get_db, get_cursor, r2d
|
||||
from auth import require_auth
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["clubs"])
|
||||
|
||||
|
||||
# ── List Clubs ────────────────────────────────────────────────────────
|
||||
@router.get("/clubs")
|
||||
def list_clubs(
|
||||
status: Optional[str] = Query(default=None),
|
||||
session=Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
List all clubs (public for authenticated users).
|
||||
|
||||
Filters:
|
||||
- status: active, inactive
|
||||
"""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
query = "SELECT * FROM clubs"
|
||||
params = []
|
||||
|
||||
if status:
|
||||
query += " WHERE status = %s"
|
||||
params.append(status)
|
||||
|
||||
query += " ORDER BY name"
|
||||
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
return [r2d(r) for r in rows]
|
||||
|
||||
|
||||
# ── Get Club ──────────────────────────────────────────────────────────
|
||||
@router.get("/clubs/{club_id}")
|
||||
def get_club(club_id: int, session=Depends(require_auth)):
|
||||
"""Get club by ID with divisions and groups."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Get club
|
||||
cur.execute("SELECT * FROM clubs WHERE id = %s", (club_id,))
|
||||
club = cur.fetchone()
|
||||
|
||||
if not club:
|
||||
raise HTTPException(404, "Verein nicht gefunden")
|
||||
|
||||
club = r2d(club)
|
||||
|
||||
# Get divisions
|
||||
cur.execute("""
|
||||
SELECT * FROM divisions
|
||||
WHERE club_id = %s
|
||||
ORDER BY name
|
||||
""", (club_id,))
|
||||
club['divisions'] = [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
# Get training groups
|
||||
cur.execute("""
|
||||
SELECT g.*,
|
||||
p.name as trainer_name
|
||||
FROM training_groups g
|
||||
LEFT JOIN profiles p ON g.trainer_id = p.id
|
||||
WHERE g.club_id = %s
|
||||
ORDER BY g.weekday, g.time_start
|
||||
""", (club_id,))
|
||||
club['training_groups'] = [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
return club
|
||||
|
||||
|
||||
# ── Create Club ───────────────────────────────────────────────────────
|
||||
@router.post("/clubs")
|
||||
def create_club(data: dict, session=Depends(require_auth)):
|
||||
"""Create new club (admin only)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Vereine erstellen")
|
||||
|
||||
name = data.get('name')
|
||||
if not name:
|
||||
raise HTTPException(400, "Name ist Pflichtfeld")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO clubs (name, abbreviation, description, status)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
name,
|
||||
data.get('abbreviation'),
|
||||
data.get('description'),
|
||||
data.get('status', 'active')
|
||||
))
|
||||
|
||||
club_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
|
||||
return get_club(club_id, session)
|
||||
|
||||
|
||||
# ── Update Club ───────────────────────────────────────────────────────
|
||||
@router.put("/clubs/{club_id}")
|
||||
def update_club(club_id: int, data: dict, session=Depends(require_auth)):
|
||||
"""Update club (admin only)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Vereine bearbeiten")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check existence
|
||||
cur.execute("SELECT id FROM clubs WHERE id = %s", (club_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Verein nicht gefunden")
|
||||
|
||||
# Update
|
||||
cur.execute("""
|
||||
UPDATE clubs SET
|
||||
name = %s,
|
||||
abbreviation = %s,
|
||||
description = %s,
|
||||
status = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (
|
||||
data.get('name'),
|
||||
data.get('abbreviation'),
|
||||
data.get('description'),
|
||||
data.get('status'),
|
||||
club_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return get_club(club_id, session)
|
||||
|
||||
|
||||
# ── Delete Club ───────────────────────────────────────────────────────
|
||||
@router.delete("/clubs/{club_id}")
|
||||
def delete_club(club_id: int, session=Depends(require_auth)):
|
||||
"""Delete club (superadmin only)."""
|
||||
role = session.get('role')
|
||||
if role != 'superadmin':
|
||||
raise HTTPException(403, "Nur Superadmins dürfen Vereine löschen")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check existence
|
||||
cur.execute("SELECT id FROM clubs WHERE id = %s", (club_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Verein nicht gefunden")
|
||||
|
||||
# Delete (CASCADE handles divisions and groups)
|
||||
cur.execute("DELETE FROM clubs WHERE id = %s", (club_id,))
|
||||
conn.commit()
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── List Divisions ────────────────────────────────────────────────────
|
||||
@router.get("/divisions")
|
||||
def list_divisions(
|
||||
club_id: Optional[int] = Query(default=None),
|
||||
session=Depends(require_auth)
|
||||
):
|
||||
"""List divisions (optional filter by club)."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
query = """
|
||||
SELECT d.*, c.name as club_name
|
||||
FROM divisions d
|
||||
LEFT JOIN clubs c ON d.club_id = c.id
|
||||
"""
|
||||
params = []
|
||||
|
||||
if club_id:
|
||||
query += " WHERE d.club_id = %s"
|
||||
params.append(club_id)
|
||||
|
||||
query += " ORDER BY d.name"
|
||||
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
return [r2d(r) for r in rows]
|
||||
|
||||
|
||||
# ── Create Division ───────────────────────────────────────────────────
|
||||
@router.post("/divisions")
|
||||
def create_division(data: dict, session=Depends(require_auth)):
|
||||
"""Create new division (admin only)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Sparten erstellen")
|
||||
|
||||
club_id = data.get('club_id')
|
||||
name = data.get('name')
|
||||
|
||||
if not club_id or not name:
|
||||
raise HTTPException(400, "club_id und name sind Pflichtfelder")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check club exists
|
||||
cur.execute("SELECT id FROM clubs WHERE id = %s", (club_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Verein nicht gefunden")
|
||||
|
||||
# Insert
|
||||
cur.execute("""
|
||||
INSERT INTO divisions (club_id, name, focus_area)
|
||||
VALUES (%s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
club_id,
|
||||
name,
|
||||
data.get('focus_area')
|
||||
))
|
||||
|
||||
division_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
|
||||
# Return created division
|
||||
cur.execute("""
|
||||
SELECT d.*, c.name as club_name
|
||||
FROM divisions d
|
||||
LEFT JOIN clubs c ON d.club_id = c.id
|
||||
WHERE d.id = %s
|
||||
""", (division_id,))
|
||||
return r2d(cur.fetchone())
|
||||
|
||||
|
||||
# ── Update Division ───────────────────────────────────────────────────
|
||||
@router.put("/divisions/{division_id}")
|
||||
def update_division(division_id: int, data: dict, session=Depends(require_auth)):
|
||||
"""Update division (admin only)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Sparten bearbeiten")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check existence
|
||||
cur.execute("SELECT id FROM divisions WHERE id = %s", (division_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Sparte nicht gefunden")
|
||||
|
||||
# Update
|
||||
cur.execute("""
|
||||
UPDATE divisions SET
|
||||
name = %s,
|
||||
focus_area = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (
|
||||
data.get('name'),
|
||||
data.get('focus_area'),
|
||||
division_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Return updated division
|
||||
cur.execute("""
|
||||
SELECT d.*, c.name as club_name
|
||||
FROM divisions d
|
||||
LEFT JOIN clubs c ON d.club_id = c.id
|
||||
WHERE d.id = %s
|
||||
""", (division_id,))
|
||||
return r2d(cur.fetchone())
|
||||
|
||||
|
||||
# ── Delete Division ───────────────────────────────────────────────────
|
||||
@router.delete("/divisions/{division_id}")
|
||||
def delete_division(division_id: int, session=Depends(require_auth)):
|
||||
"""Delete division (admin only)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Sparten löschen")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check existence
|
||||
cur.execute("SELECT id FROM divisions WHERE id = %s", (division_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Sparte nicht gefunden")
|
||||
|
||||
# Delete
|
||||
cur.execute("DELETE FROM divisions WHERE id = %s", (division_id,))
|
||||
conn.commit()
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── List Training Groups ──────────────────────────────────────────────
|
||||
@router.get("/groups")
|
||||
def list_training_groups(
|
||||
club_id: Optional[int] = Query(default=None),
|
||||
division_id: Optional[int] = Query(default=None),
|
||||
status: Optional[str] = Query(default=None),
|
||||
session=Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
List training groups with optional filters.
|
||||
|
||||
Filters:
|
||||
- club_id: Filter by club
|
||||
- division_id: Filter by division
|
||||
- status: active, inactive
|
||||
"""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
query = """
|
||||
SELECT g.*,
|
||||
c.name as club_name,
|
||||
d.name as division_name,
|
||||
p.name as trainer_name
|
||||
FROM training_groups g
|
||||
LEFT JOIN clubs c ON g.club_id = c.id
|
||||
LEFT JOIN divisions d ON g.division_id = d.id
|
||||
LEFT JOIN profiles p ON g.trainer_id = p.id
|
||||
"""
|
||||
|
||||
where = []
|
||||
params = []
|
||||
|
||||
if club_id:
|
||||
where.append("g.club_id = %s")
|
||||
params.append(club_id)
|
||||
|
||||
if division_id:
|
||||
where.append("g.division_id = %s")
|
||||
params.append(division_id)
|
||||
|
||||
if status:
|
||||
where.append("g.status = %s")
|
||||
params.append(status)
|
||||
|
||||
if where:
|
||||
query += " WHERE " + " AND ".join(where)
|
||||
|
||||
query += " ORDER BY g.weekday, g.time_start"
|
||||
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
return [r2d(r) for r in rows]
|
||||
|
||||
|
||||
# ── Get Training Group ────────────────────────────────────────────────
|
||||
@router.get("/groups/{group_id}")
|
||||
def get_training_group(group_id: int, session=Depends(require_auth)):
|
||||
"""Get training group by ID."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
cur.execute("""
|
||||
SELECT g.*,
|
||||
c.name as club_name,
|
||||
d.name as division_name,
|
||||
p.name as trainer_name
|
||||
FROM training_groups g
|
||||
LEFT JOIN clubs c ON g.club_id = c.id
|
||||
LEFT JOIN divisions d ON g.division_id = d.id
|
||||
LEFT JOIN profiles p ON g.trainer_id = p.id
|
||||
WHERE g.id = %s
|
||||
""", (group_id,))
|
||||
|
||||
group = cur.fetchone()
|
||||
|
||||
if not group:
|
||||
raise HTTPException(404, "Trainingsgruppe nicht gefunden")
|
||||
|
||||
return r2d(group)
|
||||
|
||||
|
||||
# ── Create Training Group ─────────────────────────────────────────────
|
||||
@router.post("/groups")
|
||||
def create_training_group(data: dict, session=Depends(require_auth)):
|
||||
"""Create new training group (admin or trainer)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin', 'trainer']:
|
||||
raise HTTPException(403, "Nur Admins und Trainer dürfen Gruppen erstellen")
|
||||
|
||||
club_id = data.get('club_id')
|
||||
name = data.get('name')
|
||||
|
||||
if not club_id or not name:
|
||||
raise HTTPException(400, "club_id und name sind Pflichtfelder")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check club exists
|
||||
cur.execute("SELECT id FROM clubs WHERE id = %s", (club_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Verein nicht gefunden")
|
||||
|
||||
# Insert
|
||||
cur.execute("""
|
||||
INSERT INTO training_groups (
|
||||
club_id, division_id, name, focus, level, age_group,
|
||||
weekday, time_start, time_end, location,
|
||||
trainer_id, co_trainer_ids, status
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s, %s, %s
|
||||
) RETURNING id
|
||||
""", (
|
||||
club_id,
|
||||
data.get('division_id'),
|
||||
name,
|
||||
data.get('focus'),
|
||||
data.get('level'),
|
||||
data.get('age_group'),
|
||||
data.get('weekday'),
|
||||
data.get('time_start'),
|
||||
data.get('time_end'),
|
||||
data.get('location'),
|
||||
data.get('trainer_id'),
|
||||
data.get('co_trainer_ids'),
|
||||
data.get('status', 'active')
|
||||
))
|
||||
|
||||
group_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
|
||||
return get_training_group(group_id, session)
|
||||
|
||||
|
||||
# ── Update Training Group ─────────────────────────────────────────────
|
||||
@router.put("/groups/{group_id}")
|
||||
def update_training_group(group_id: int, data: dict, session=Depends(require_auth)):
|
||||
"""Update training group (admin or assigned trainer)."""
|
||||
profile_id = session['profile_id']
|
||||
role = session.get('role')
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check existence and ownership
|
||||
cur.execute("SELECT trainer_id FROM training_groups WHERE id = %s", (group_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Trainingsgruppe nicht gefunden")
|
||||
|
||||
# Only admin or assigned trainer can update
|
||||
if role not in ['admin', 'superadmin'] and row['trainer_id'] != profile_id:
|
||||
raise HTTPException(403, "Keine Berechtigung")
|
||||
|
||||
# Update
|
||||
cur.execute("""
|
||||
UPDATE training_groups SET
|
||||
name = %s,
|
||||
division_id = %s,
|
||||
focus = %s,
|
||||
level = %s,
|
||||
age_group = %s,
|
||||
weekday = %s,
|
||||
time_start = %s,
|
||||
time_end = %s,
|
||||
location = %s,
|
||||
trainer_id = %s,
|
||||
co_trainer_ids = %s,
|
||||
status = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (
|
||||
data.get('name'),
|
||||
data.get('division_id'),
|
||||
data.get('focus'),
|
||||
data.get('level'),
|
||||
data.get('age_group'),
|
||||
data.get('weekday'),
|
||||
data.get('time_start'),
|
||||
data.get('time_end'),
|
||||
data.get('location'),
|
||||
data.get('trainer_id'),
|
||||
data.get('co_trainer_ids'),
|
||||
data.get('status'),
|
||||
group_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return get_training_group(group_id, session)
|
||||
|
||||
|
||||
# ── Delete Training Group ─────────────────────────────────────────────
|
||||
@router.delete("/groups/{group_id}")
|
||||
def delete_training_group(group_id: int, session=Depends(require_auth)):
|
||||
"""Delete training group (admin only)."""
|
||||
role = session.get('role')
|
||||
if role not in ['admin', 'superadmin']:
|
||||
raise HTTPException(403, "Nur Admins dürfen Gruppen löschen")
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Check existence
|
||||
cur.execute("SELECT id FROM training_groups WHERE id = %s", (group_id,))
|
||||
if not cur.fetchone():
|
||||
raise HTTPException(404, "Trainingsgruppe nicht gefunden")
|
||||
|
||||
# Delete
|
||||
cur.execute("DELETE FROM training_groups WHERE id = %s", (group_id,))
|
||||
conn.commit()
|
||||
|
||||
return {"ok": True}
|
||||
|
|
@ -1,14 +1,688 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import api from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
function ClubsPage() {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '2rem' }}>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1>Vereinsverwaltung</h1>
|
||||
<h1 style={{ marginBottom: '1.5rem' }}>Vereinsverwaltung</h1>
|
||||
|
||||
<div className="card" style={{ marginTop: '1.5rem' }}>
|
||||
<p style={{ color: 'var(--text2)' }}>
|
||||
Vereinsverwaltung folgt in Kürze
|
||||
</p>
|
||||
{/* 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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ export async function listClubs() {
|
|||
return request('/api/clubs')
|
||||
}
|
||||
|
||||
export async function getClub(id) {
|
||||
return request(`/api/clubs/${id}`)
|
||||
}
|
||||
|
||||
export async function createClub(data) {
|
||||
return request('/api/clubs', {
|
||||
method: 'POST',
|
||||
|
|
@ -75,9 +79,47 @@ export async function createClub(data) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function listTrainingGroups(clubId) {
|
||||
export async function updateClub(id, data) {
|
||||
return request(`/api/clubs/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteClub(id) {
|
||||
return request(`/api/clubs/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export async function listDivisions(clubId) {
|
||||
const query = clubId ? `?club_id=${clubId}` : ''
|
||||
return request(`/api/groups${query}`)
|
||||
return request(`/api/divisions${query}`)
|
||||
}
|
||||
|
||||
export async function createDivision(data) {
|
||||
return request('/api/divisions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateDivision(id, data) {
|
||||
return request(`/api/divisions/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteDivision(id) {
|
||||
return request(`/api/divisions/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export async function listTrainingGroups(filters = {}) {
|
||||
const query = new URLSearchParams(filters).toString()
|
||||
return request(`/api/groups${query ? '?' + query : ''}`)
|
||||
}
|
||||
|
||||
export async function getTrainingGroup(id) {
|
||||
return request(`/api/groups/${id}`)
|
||||
}
|
||||
|
||||
export async function createTrainingGroup(data) {
|
||||
|
|
@ -87,6 +129,17 @@ export async function createTrainingGroup(data) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function updateTrainingGroup(id, data) {
|
||||
return request(`/api/groups/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteTrainingGroup(id) {
|
||||
return request(`/api/groups/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Skills & Methods
|
||||
// ============================================================================
|
||||
|
|
@ -192,9 +245,19 @@ export const api = {
|
|||
|
||||
// Clubs & Groups
|
||||
listClubs,
|
||||
getClub,
|
||||
createClub,
|
||||
updateClub,
|
||||
deleteClub,
|
||||
listDivisions,
|
||||
createDivision,
|
||||
updateDivision,
|
||||
deleteDivision,
|
||||
listTrainingGroups,
|
||||
getTrainingGroup,
|
||||
createTrainingGroup,
|
||||
updateTrainingGroup,
|
||||
deleteTrainingGroup,
|
||||
|
||||
// Skills & Methods
|
||||
listSkills,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user