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)
529 lines
17 KiB
Python
529 lines
17 KiB
Python
"""
|
|
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}
|