- Updated role permissions to allow trainers and users to create clubs and training groups. - Modified database insertion logic to reflect the correct role for trainers during registration. - Enhanced frontend components to display appropriate messages and buttons based on user roles. - Improved user guidance in the Clubs and Training Planning pages, emphasizing the need for clubs and groups before planning training sessions.
534 lines
17 KiB
Python
534 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 oder Trainer — MVP ohne separates Vereins-Onboarding)."""
|
|
role = session.get('role')
|
|
if role not in ['admin', 'superadmin', 'trainer', 'user']:
|
|
raise HTTPException(403, "Keine Berechtigung, Vereine anzulegen")
|
|
|
|
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, trainer oder normaler Nutzer mit Vereinsbezug)."""
|
|
profile_id = session["profile_id"]
|
|
role = session.get('role')
|
|
if role not in ['admin', 'superadmin', 'trainer', 'user']:
|
|
raise HTTPException(403, "Keine Berechtigung, Trainingsgruppen anzulegen")
|
|
|
|
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")
|
|
|
|
trainer_id = data.get('trainer_id')
|
|
if trainer_id in (None, "", 0) and role in ("trainer", "user"):
|
|
trainer_id = profile_id
|
|
|
|
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'),
|
|
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}
|