feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Catalog Management Endpoints for Shinkan Jinkendo
|
|
|
|
|
|
|
|
|
|
|
|
Admin-verwaltbare Stammdaten für Übungen, Fokusbereiche, Stile, etc.
|
|
|
|
|
|
"""
|
2026-04-27 11:53:25 +02:00
|
|
|
|
import re
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
from typing import Optional
|
2026-04-27 11:53:25 +02:00
|
|
|
|
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
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=["catalogs"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 15:01:47 +02:00
|
|
|
|
def _sql_active_status(column: str, status: Optional[str]) -> tuple[str, list]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Filter „active“ schließt Legacy-Zeilen mit status IS NULL ein (sonst leere Dropdowns in der UI).
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not status:
|
|
|
|
|
|
return "", []
|
|
|
|
|
|
if status == "active":
|
|
|
|
|
|
return f" ({column} = 'active' OR {column} IS NULL)", []
|
|
|
|
|
|
return f" ({column} = %s)", [status]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
def _slugify_skill_label(text: str) -> str:
|
|
|
|
|
|
t = (text or "").strip().lower()
|
|
|
|
|
|
t = re.sub(r"[^a-z0-9äöüß]+", "_", t, flags=re.IGNORECASE)
|
|
|
|
|
|
t = re.sub(r"_+", "_", t).strip("_")
|
|
|
|
|
|
return (t[:48] or "gruppe")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# SKILL MAIN CATEGORIES (Hauptgruppen, z. B. „KARATE Fähigkeiten“)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/skill-main-categories")
|
|
|
|
|
|
def list_skill_main_categories(session=Depends(require_auth)):
|
|
|
|
|
|
"""Alle Hauptkategorien für den Fähigkeitskatalog (sortiert)."""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"SELECT * FROM skill_main_categories ORDER BY sort_order NULLS LAST, name"
|
|
|
|
|
|
)
|
|
|
|
|
|
return [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/skill-main-categories")
|
|
|
|
|
|
def create_skill_main_category(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
role = session.get("role")
|
|
|
|
|
|
if role not in ("admin", "superadmin"):
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Hauptkategorien anlegen")
|
|
|
|
|
|
name = (data.get("name") or "").strip()
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
raise HTTPException(400, "Name ist Pflichtfeld")
|
|
|
|
|
|
slug = (data.get("slug") or "").strip() or _slugify_skill_label(name)
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
try:
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO skill_main_categories (name, slug, description, sort_order)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s)
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""",
|
|
|
|
|
|
(
|
|
|
|
|
|
name,
|
|
|
|
|
|
slug,
|
|
|
|
|
|
data.get("description"),
|
|
|
|
|
|
data.get("sort_order", 99),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
mid = cur.fetchone()["id"]
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if "unique" in str(e).lower() or "duplicate" in str(e).lower():
|
|
|
|
|
|
raise HTTPException(409, "Name oder Slug bereits vergeben") from e
|
|
|
|
|
|
raise
|
|
|
|
|
|
cur.execute("SELECT * FROM skill_main_categories WHERE id = %s", (mid,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/skill-main-categories/{main_id}")
|
|
|
|
|
|
def update_skill_main_category(main_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
role = session.get("role")
|
|
|
|
|
|
if role not in ("admin", "superadmin"):
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Hauptkategorien bearbeiten")
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute("SELECT id FROM skill_main_categories WHERE id = %s", (main_id,))
|
|
|
|
|
|
if not cur.fetchone():
|
|
|
|
|
|
raise HTTPException(404, "Hauptkategorie nicht gefunden")
|
|
|
|
|
|
sets = []
|
|
|
|
|
|
vals = []
|
|
|
|
|
|
for key in ("name", "slug", "description", "sort_order"):
|
|
|
|
|
|
if key in data:
|
|
|
|
|
|
sets.append(f"{key} = %s")
|
|
|
|
|
|
vals.append(data[key])
|
|
|
|
|
|
if not sets:
|
|
|
|
|
|
cur.execute("SELECT * FROM skill_main_categories WHERE id = %s", (main_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
sets.append("updated_at = NOW()")
|
|
|
|
|
|
vals.append(main_id)
|
|
|
|
|
|
try:
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
f"UPDATE skill_main_categories SET {', '.join(sets)} WHERE id = %s",
|
|
|
|
|
|
tuple(vals),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if "unique" in str(e).lower():
|
|
|
|
|
|
raise HTTPException(409, "Name oder Slug bereits vergeben") from e
|
|
|
|
|
|
raise
|
|
|
|
|
|
cur.execute("SELECT * FROM skill_main_categories WHERE id = %s", (main_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/skill-main-categories/{main_id}")
|
|
|
|
|
|
def delete_skill_main_category(main_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
role = session.get("role")
|
|
|
|
|
|
if role != "superadmin":
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Hauptkategorien löschen")
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"SELECT COUNT(*) AS c FROM skill_categories WHERE main_category_id = %s",
|
|
|
|
|
|
(main_id,),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (cur.fetchone() or {}).get("c", 0) > 0:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
409,
|
|
|
|
|
|
"Hauptkategorie hat noch Unterkategorien. Bitte zuerst verschieben oder löschen.",
|
|
|
|
|
|
)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"SELECT COUNT(*) AS c FROM skills WHERE main_category_id = %s",
|
|
|
|
|
|
(main_id,),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (cur.fetchone() or {}).get("c", 0) > 0:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
409,
|
|
|
|
|
|
"Hauptkategorie ist noch Fähigkeiten zugeordnet.",
|
|
|
|
|
|
)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"DELETE FROM skill_main_categories WHERE id = %s RETURNING id",
|
|
|
|
|
|
(main_id,),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not cur.fetchone():
|
|
|
|
|
|
raise HTTPException(404, "Hauptkategorie nicht gefunden")
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# FOCUS AREAS
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/focus-areas")
|
|
|
|
|
|
def list_focus_areas(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""List all focus areas (public for authenticated users)."""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
query = "SELECT * FROM focus_areas"
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
2026-04-27 15:01:47 +02:00
|
|
|
|
frag, extra = _sql_active_status("status", status)
|
|
|
|
|
|
if frag:
|
|
|
|
|
|
query += " WHERE" + frag
|
|
|
|
|
|
params.extend(extra)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY sort_order, name"
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/focus-areas")
|
|
|
|
|
|
def create_focus_area(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Create new focus area (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Fokusbereiche 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 focus_areas (name, abbreviation, description, color, icon, sort_order, status)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
name,
|
|
|
|
|
|
data.get('abbreviation'),
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('color'),
|
|
|
|
|
|
data.get('icon'),
|
|
|
|
|
|
data.get('sort_order', 99),
|
|
|
|
|
|
data.get('status', 'active')
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
focus_area_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("SELECT * FROM focus_areas WHERE id = %s", (focus_area_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/focus-areas/{focus_area_id}")
|
|
|
|
|
|
def update_focus_area(focus_area_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update focus area (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Fokusbereiche bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
UPDATE focus_areas SET
|
|
|
|
|
|
name = %s,
|
|
|
|
|
|
abbreviation = %s,
|
|
|
|
|
|
description = %s,
|
|
|
|
|
|
color = %s,
|
|
|
|
|
|
icon = %s,
|
|
|
|
|
|
sort_order = %s,
|
|
|
|
|
|
status = %s,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('name'),
|
|
|
|
|
|
data.get('abbreviation'),
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('color'),
|
|
|
|
|
|
data.get('icon'),
|
|
|
|
|
|
data.get('sort_order'),
|
|
|
|
|
|
data.get('status'),
|
|
|
|
|
|
focus_area_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("SELECT * FROM focus_areas WHERE id = %s", (focus_area_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/focus-areas/{focus_area_id}")
|
|
|
|
|
|
def delete_focus_area(focus_area_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete focus area (superadmin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role != 'superadmin':
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Fokusbereiche löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute("DELETE FROM focus_areas WHERE id = %s", (focus_area_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TRAINING STYLES
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/training-styles")
|
|
|
|
|
|
def list_training_styles(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""List all training styles."""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
2026-04-23 14:36:02 +02:00
|
|
|
|
SELECT ts.*, ps.name as parent_style_name, fa.name as focus_area_name, fa.icon as focus_area_icon
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_directions ts
|
|
|
|
|
|
LEFT JOIN style_directions ps ON ts.parent_style_id = ps.id
|
2026-04-23 14:36:02 +02:00
|
|
|
|
LEFT JOIN focus_areas fa ON ts.focus_area_id = fa.id
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
"""
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
2026-04-27 15:01:47 +02:00
|
|
|
|
frag, extra = _sql_active_status("ts.status", status)
|
|
|
|
|
|
if frag:
|
|
|
|
|
|
query += " WHERE" + frag
|
|
|
|
|
|
params.extend(extra)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY ts.sort_order, ts.name"
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/training-styles")
|
|
|
|
|
|
def create_training_style(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Create new training style (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingsstile 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("""
|
2026-04-23 14:36:02 +02:00
|
|
|
|
INSERT INTO style_directions (name, abbreviation, description, focus_area_id, parent_style_id, sort_order, status)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
name,
|
|
|
|
|
|
data.get('abbreviation'),
|
|
|
|
|
|
data.get('description'),
|
2026-04-23 14:36:02 +02:00
|
|
|
|
data.get('focus_area_id'),
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
data.get('parent_style_id'),
|
|
|
|
|
|
data.get('sort_order', 99),
|
|
|
|
|
|
data.get('status', 'active')
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
style_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
2026-04-23 14:36:02 +02:00
|
|
|
|
SELECT ts.*, ps.name as parent_style_name, fa.name as focus_area_name, fa.icon as focus_area_icon
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_directions ts
|
|
|
|
|
|
LEFT JOIN style_directions ps ON ts.parent_style_id = ps.id
|
2026-04-23 14:36:02 +02:00
|
|
|
|
LEFT JOIN focus_areas fa ON ts.focus_area_id = fa.id
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
WHERE ts.id = %s
|
|
|
|
|
|
""", (style_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/training-styles/{style_id}")
|
|
|
|
|
|
def update_training_style(style_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update training style (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingsstile bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
2026-04-23 12:24:50 +02:00
|
|
|
|
UPDATE style_directions SET
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
name = %s,
|
|
|
|
|
|
abbreviation = %s,
|
|
|
|
|
|
description = %s,
|
2026-04-23 14:36:02 +02:00
|
|
|
|
focus_area_id = %s,
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
parent_style_id = %s,
|
|
|
|
|
|
sort_order = %s,
|
|
|
|
|
|
status = %s,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('name'),
|
|
|
|
|
|
data.get('abbreviation'),
|
|
|
|
|
|
data.get('description'),
|
2026-04-23 14:36:02 +02:00
|
|
|
|
data.get('focus_area_id'),
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
data.get('parent_style_id'),
|
|
|
|
|
|
data.get('sort_order'),
|
|
|
|
|
|
data.get('status'),
|
|
|
|
|
|
style_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
2026-04-23 14:36:02 +02:00
|
|
|
|
SELECT ts.*, ps.name as parent_style_name, fa.name as focus_area_name, fa.icon as focus_area_icon
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_directions ts
|
|
|
|
|
|
LEFT JOIN style_directions ps ON ts.parent_style_id = ps.id
|
2026-04-23 14:36:02 +02:00
|
|
|
|
LEFT JOIN focus_areas fa ON ts.focus_area_id = fa.id
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
WHERE ts.id = %s
|
|
|
|
|
|
""", (style_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/training-styles/{style_id}")
|
|
|
|
|
|
def delete_training_style(style_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete training style (superadmin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role != 'superadmin':
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Trainingsstile löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
2026-04-23 12:24:50 +02:00
|
|
|
|
cur.execute("DELETE FROM style_directions WHERE id = %s", (style_id,))
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TRAINING CHARACTERS
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/training-characters")
|
|
|
|
|
|
def list_training_characters(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""List all training characters."""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
query = "SELECT * FROM training_characters"
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
|
|
if status:
|
|
|
|
|
|
query += " WHERE status = %s"
|
|
|
|
|
|
params.append(status)
|
|
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY sort_order, name"
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/training-characters")
|
|
|
|
|
|
def create_training_character(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Create new training character (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingscharaktere 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 training_characters (name, description, sort_order, status)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s)
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
name,
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('sort_order', 99),
|
|
|
|
|
|
data.get('status', 'active')
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
char_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("SELECT * FROM training_characters WHERE id = %s", (char_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/training-characters/{char_id}")
|
|
|
|
|
|
def update_training_character(char_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update training character (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingscharaktere bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
UPDATE training_characters SET
|
|
|
|
|
|
name = %s,
|
|
|
|
|
|
description = %s,
|
|
|
|
|
|
sort_order = %s,
|
|
|
|
|
|
status = %s,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('name'),
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('sort_order'),
|
|
|
|
|
|
data.get('status'),
|
|
|
|
|
|
char_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("SELECT * FROM training_characters WHERE id = %s", (char_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/training-characters/{char_id}")
|
|
|
|
|
|
def delete_training_character(char_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete training character (superadmin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role != 'superadmin':
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Trainingscharaktere löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute("DELETE FROM training_characters WHERE id = %s", (char_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 12:12:48 +02:00
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TRAINING TYPES (Breitensport, Leistungssport, etc.)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/training-types")
|
|
|
|
|
|
def list_training_types(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
2026-04-23 15:29:23 +02:00
|
|
|
|
focus_area_id: Optional[int] = Query(default=None),
|
2026-04-23 12:12:48 +02:00
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
2026-04-23 15:29:23 +02:00
|
|
|
|
"""List all training types.
|
|
|
|
|
|
|
|
|
|
|
|
Optional filter by focus_area_id for context-specific types.
|
|
|
|
|
|
"""
|
2026-04-23 12:12:48 +02:00
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
2026-04-23 15:29:23 +02:00
|
|
|
|
query = """
|
|
|
|
|
|
SELECT tt.*, fa.name as focus_area_name, fa.icon as focus_area_icon
|
|
|
|
|
|
FROM training_types tt
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tt.focus_area_id = fa.id
|
|
|
|
|
|
"""
|
2026-04-23 12:12:48 +02:00
|
|
|
|
params = []
|
2026-04-23 15:29:23 +02:00
|
|
|
|
where = []
|
2026-04-23 12:12:48 +02:00
|
|
|
|
|
2026-04-27 15:01:47 +02:00
|
|
|
|
frag, extra = _sql_active_status("tt.status", status)
|
|
|
|
|
|
if frag:
|
|
|
|
|
|
where.append(frag.strip())
|
|
|
|
|
|
params.extend(extra)
|
2026-04-23 12:12:48 +02:00
|
|
|
|
|
2026-04-23 15:29:23 +02:00
|
|
|
|
if focus_area_id is not None:
|
|
|
|
|
|
where.append("tt.focus_area_id = %s")
|
|
|
|
|
|
params.append(focus_area_id)
|
|
|
|
|
|
|
|
|
|
|
|
if where:
|
|
|
|
|
|
query += " WHERE " + " AND ".join(where)
|
|
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY fa.sort_order, tt.sort_order, tt.name"
|
2026-04-23 12:12:48 +02:00
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/training-types")
|
|
|
|
|
|
def create_training_type(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Create new training type (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingsstile 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("""
|
2026-04-23 15:29:23 +02:00
|
|
|
|
INSERT INTO training_types (name, abbreviation, description, focus_area_id, sort_order, status)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
2026-04-23 12:12:48 +02:00
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
name,
|
|
|
|
|
|
data.get('abbreviation'),
|
|
|
|
|
|
data.get('description'),
|
2026-04-23 15:29:23 +02:00
|
|
|
|
data.get('focus_area_id'),
|
2026-04-23 12:12:48 +02:00
|
|
|
|
data.get('sort_order', 99),
|
|
|
|
|
|
data.get('status', 'active')
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
type_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
2026-04-23 15:29:23 +02:00
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT tt.*, fa.name as focus_area_name, fa.icon as focus_area_icon
|
|
|
|
|
|
FROM training_types tt
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tt.focus_area_id = fa.id
|
|
|
|
|
|
WHERE tt.id = %s
|
|
|
|
|
|
""", (type_id,))
|
2026-04-23 12:12:48 +02:00
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/training-types/{type_id}")
|
|
|
|
|
|
def update_training_type(type_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update training type (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingsstile bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
UPDATE training_types SET
|
|
|
|
|
|
name = %s,
|
|
|
|
|
|
abbreviation = %s,
|
|
|
|
|
|
description = %s,
|
2026-04-23 15:29:23 +02:00
|
|
|
|
focus_area_id = %s,
|
2026-04-23 12:12:48 +02:00
|
|
|
|
sort_order = %s,
|
|
|
|
|
|
status = %s,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('name'),
|
|
|
|
|
|
data.get('abbreviation'),
|
|
|
|
|
|
data.get('description'),
|
2026-04-23 15:29:23 +02:00
|
|
|
|
data.get('focus_area_id'),
|
2026-04-23 12:12:48 +02:00
|
|
|
|
data.get('sort_order'),
|
|
|
|
|
|
data.get('status'),
|
|
|
|
|
|
type_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
2026-04-23 15:29:23 +02:00
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT tt.*, fa.name as focus_area_name, fa.icon as focus_area_icon
|
|
|
|
|
|
FROM training_types tt
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tt.focus_area_id = fa.id
|
|
|
|
|
|
WHERE tt.id = %s
|
|
|
|
|
|
""", (type_id,))
|
2026-04-23 12:12:48 +02:00
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/training-types/{type_id}")
|
|
|
|
|
|
def delete_training_type(type_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete training type (superadmin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role != 'superadmin':
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Trainingsstile löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Check if assigned to exercises
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT COUNT(*) as count
|
|
|
|
|
|
FROM exercise_training_types
|
|
|
|
|
|
WHERE training_type_id = %s
|
|
|
|
|
|
""", (type_id,))
|
|
|
|
|
|
ex_count = cur.fetchone()['count']
|
|
|
|
|
|
|
|
|
|
|
|
if ex_count > 0:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
409,
|
|
|
|
|
|
f"Trainingsstil kann nicht gelöscht werden: {ex_count} Übung(en) zugeordnet. "
|
|
|
|
|
|
"Bitte zuerst alle Zuordnungen entfernen."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("DELETE FROM training_types WHERE id = %s", (type_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# SKILL CATEGORIES
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/skill-categories")
|
|
|
|
|
|
def list_skill_categories(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
2026-04-27 11:53:25 +02:00
|
|
|
|
main_category_id: Optional[int] = Query(default=None),
|
|
|
|
|
|
session=Depends(require_auth),
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
):
|
2026-04-27 11:53:25 +02:00
|
|
|
|
"""List all skill categories (mit Hauptkategorie)."""
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
2026-04-27 11:53:25 +02:00
|
|
|
|
SELECT sc.*, pc.name as parent_category_name,
|
|
|
|
|
|
mc.id as main_category_ref_id, mc.name as main_category_name,
|
|
|
|
|
|
mc.slug as main_category_slug, mc.sort_order as main_category_sort
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
FROM skill_categories sc
|
|
|
|
|
|
LEFT JOIN skill_categories pc ON sc.parent_category_id = pc.id
|
2026-04-27 11:53:25 +02:00
|
|
|
|
LEFT JOIN skill_main_categories mc ON sc.main_category_id = mc.id
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
"""
|
|
|
|
|
|
params = []
|
2026-04-27 11:53:25 +02:00
|
|
|
|
where = []
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
|
|
|
|
|
if status:
|
2026-04-27 11:53:25 +02:00
|
|
|
|
where.append("sc.status = %s")
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
params.append(status)
|
2026-04-27 11:53:25 +02:00
|
|
|
|
if main_category_id is not None:
|
|
|
|
|
|
where.append("sc.main_category_id = %s")
|
|
|
|
|
|
params.append(main_category_id)
|
|
|
|
|
|
|
|
|
|
|
|
if where:
|
|
|
|
|
|
query += " WHERE " + " AND ".join(where)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
query += " ORDER BY mc.sort_order NULLS LAST, sc.sort_order NULLS LAST, sc.name"
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/skill-categories")
|
|
|
|
|
|
def create_skill_category(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Create new skill category (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Fähigkeitsbereiche erstellen")
|
|
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
name = (data.get("name") or "").strip()
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
if not name:
|
|
|
|
|
|
raise HTTPException(400, "Name ist Pflichtfeld")
|
|
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
main_category_id = data.get("main_category_id")
|
|
|
|
|
|
if main_category_id is not None:
|
|
|
|
|
|
main_category_id = int(main_category_id)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
slug = (data.get("slug") or "").strip() or _slugify_skill_label(name)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
try:
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO skill_categories (
|
|
|
|
|
|
name, slug, description, parent_category_id, main_category_id,
|
|
|
|
|
|
sort_order, status
|
|
|
|
|
|
)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""",
|
|
|
|
|
|
(
|
|
|
|
|
|
name,
|
|
|
|
|
|
slug,
|
|
|
|
|
|
data.get("description"),
|
|
|
|
|
|
data.get("parent_category_id"),
|
|
|
|
|
|
main_category_id,
|
|
|
|
|
|
data.get("sort_order", 99),
|
|
|
|
|
|
data.get("status", "active"),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
cat_id = cur.fetchone()["id"]
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if "unique" in str(e).lower() or "duplicate" in str(e).lower():
|
|
|
|
|
|
raise HTTPException(409, "Name oder Slug schon vergeben") from e
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT sc.*, pc.name as parent_category_name,
|
|
|
|
|
|
mc.name as main_category_name, mc.slug as main_category_slug
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
FROM skill_categories sc
|
|
|
|
|
|
LEFT JOIN skill_categories pc ON sc.parent_category_id = pc.id
|
2026-04-27 11:53:25 +02:00
|
|
|
|
LEFT JOIN skill_main_categories mc ON sc.main_category_id = mc.id
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
WHERE sc.id = %s
|
2026-04-27 11:53:25 +02:00
|
|
|
|
""",
|
|
|
|
|
|
(cat_id,),
|
|
|
|
|
|
)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/skill-categories/{cat_id}")
|
|
|
|
|
|
def update_skill_category(cat_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update skill category (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Fähigkeitsbereiche bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
cur.execute("SELECT id FROM skill_categories WHERE id = %s", (cat_id,))
|
|
|
|
|
|
if not cur.fetchone():
|
|
|
|
|
|
raise HTTPException(404, "Kategorie nicht gefunden")
|
|
|
|
|
|
|
|
|
|
|
|
sets: list = []
|
|
|
|
|
|
vals: list = []
|
|
|
|
|
|
for key in (
|
|
|
|
|
|
"name",
|
|
|
|
|
|
"slug",
|
|
|
|
|
|
"description",
|
|
|
|
|
|
"parent_category_id",
|
|
|
|
|
|
"main_category_id",
|
|
|
|
|
|
"sort_order",
|
|
|
|
|
|
"status",
|
|
|
|
|
|
):
|
|
|
|
|
|
if key in data:
|
|
|
|
|
|
sets.append(f"{key} = %s")
|
|
|
|
|
|
vals.append(data[key])
|
|
|
|
|
|
if sets:
|
|
|
|
|
|
sets.append("updated_at = NOW()")
|
|
|
|
|
|
vals.append(cat_id)
|
|
|
|
|
|
try:
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
f"UPDATE skill_categories SET {', '.join(sets)} WHERE id = %s",
|
|
|
|
|
|
tuple(vals),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
if "unique" in str(e).lower():
|
|
|
|
|
|
raise HTTPException(409, "Name oder Slug schon vergeben") from e
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"SELECT main_category_id FROM skill_categories WHERE id = %s",
|
|
|
|
|
|
(cat_id,),
|
|
|
|
|
|
)
|
|
|
|
|
|
effective_main = cur.fetchone()["main_category_id"]
|
|
|
|
|
|
if effective_main is not None:
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"UPDATE skills SET main_category_id = %s WHERE category_id = %s",
|
|
|
|
|
|
(effective_main, cat_id),
|
|
|
|
|
|
)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
2026-04-27 11:53:25 +02:00
|
|
|
|
cur.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT sc.*, pc.name as parent_category_name,
|
|
|
|
|
|
mc.name as main_category_name, mc.slug as main_category_slug
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
FROM skill_categories sc
|
|
|
|
|
|
LEFT JOIN skill_categories pc ON sc.parent_category_id = pc.id
|
2026-04-27 11:53:25 +02:00
|
|
|
|
LEFT JOIN skill_main_categories mc ON sc.main_category_id = mc.id
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
WHERE sc.id = %s
|
2026-04-27 11:53:25 +02:00
|
|
|
|
""",
|
|
|
|
|
|
(cat_id,),
|
|
|
|
|
|
)
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/skill-categories/{cat_id}")
|
|
|
|
|
|
def delete_skill_category(cat_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete skill category (superadmin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role != 'superadmin':
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Fähigkeitsbereiche löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
2026-04-27 11:53:25 +02:00
|
|
|
|
cur.execute("SELECT COUNT(*) AS c FROM skills WHERE category_id = %s", (cat_id,))
|
|
|
|
|
|
if (cur.fetchone() or {}).get("c", 0) > 0:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
409,
|
|
|
|
|
|
"Kategorie noch Fähigkeiten zugeordnet – bitte zuerst verschieben oder löschen.",
|
|
|
|
|
|
)
|
|
|
|
|
|
cur.execute(
|
|
|
|
|
|
"SELECT COUNT(*) AS c FROM skill_categories WHERE parent_category_id = %s",
|
|
|
|
|
|
(cat_id,),
|
|
|
|
|
|
)
|
|
|
|
|
|
if (cur.fetchone() or {}).get("c", 0) > 0:
|
|
|
|
|
|
raise HTTPException(409, "Kategorie hat Unterkategorien.")
|
|
|
|
|
|
cur.execute("DELETE FROM skill_categories WHERE id = %s RETURNING id", (cat_id,))
|
|
|
|
|
|
if not cur.fetchone():
|
|
|
|
|
|
raise HTTPException(404, "Kategorie nicht gefunden")
|
feat: Exercise Catalogs - Admin-verwaltbare Stammdaten (Backend)
Problem: Hard-codierte Werte (Fokusbereich, Trainingscharakter) + fehlende
Dimensionen (Stil, Fähigkeiten-Matrix) + keine Rollen-basierte Sichtbarkeit
Lösung: Dynamische Kataloge mit Admin-CRUD
Migration 007_exercise_catalogs.sql:
- focus_areas (statt hard-coded 'karate', 'selbstverteidigung', 'gewaltschutz')
- training_styles (NEU: Shotokan, Goju-Ryu, Wado-Ryu, etc. mit Hierarchie)
- training_characters (statt hard-coded 'grundlage', 'aufbau', etc.)
- skill_categories (Matrix: Kategorien → Einzelfähigkeiten)
- trainer_focus_areas (Zuordnung: Trainer → Fokusbereiche)
- exercises erweitert: training_style_id, training_character_id, focus_area_id
- skills erweitert: category_id, parent_skill_id, level, sort_order
- Seed-Daten für alle Kataloge
Backend (routers/catalogs.py):
- CRUD für focus_areas (admin only)
- CRUD für training_styles (admin only, mit parent_style_id)
- CRUD für training_characters (admin only)
- CRUD für skill_categories (admin only, mit parent_category_id)
- CRUD für trainer_focus_areas (admin: assign, trainer: read own)
- Alle mit status-Filter (active/inactive)
Backend (routers/exercises.py):
- CREATE/UPDATE erweitert um training_style_id, training_character_id, focus_area_id
- Legacy-Felder (focus_area text, training_character text) bleiben parallel
Backend (main.py):
- catalogs Router registriert
Nächster Schritt: Frontend-UI (Admin-Kataloge + Exercise-Formular-Update)
2026-04-22 22:06:11 +02:00
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TRAINER FOCUS AREAS (Welcher Trainer arbeitet in welchen Fokusbereichen?)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/trainer-focus-areas")
|
|
|
|
|
|
def list_trainer_focus_areas(
|
|
|
|
|
|
profile_id: Optional[int] = Query(default=None),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""List trainer focus area assignments."""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
|
|
|
SELECT tfa.*, fa.name as focus_area_name, fa.abbreviation as focus_area_abbr,
|
|
|
|
|
|
p.name as trainer_name
|
|
|
|
|
|
FROM trainer_focus_areas tfa
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tfa.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN profiles p ON tfa.profile_id = p.id
|
|
|
|
|
|
"""
|
|
|
|
|
|
params = []
|
|
|
|
|
|
|
|
|
|
|
|
# If not admin, only show own focus areas
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
current_profile_id = session['profile_id']
|
|
|
|
|
|
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
query += " WHERE tfa.profile_id = %s"
|
|
|
|
|
|
params.append(current_profile_id)
|
|
|
|
|
|
elif profile_id:
|
|
|
|
|
|
query += " WHERE tfa.profile_id = %s"
|
|
|
|
|
|
params.append(profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
query += " ORDER BY fa.name"
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/trainer-focus-areas")
|
|
|
|
|
|
def assign_trainer_focus_area(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Assign focus area to trainer (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Fokusbereiche zuweisen")
|
|
|
|
|
|
|
|
|
|
|
|
profile_id = data.get('profile_id')
|
|
|
|
|
|
focus_area_id = data.get('focus_area_id')
|
|
|
|
|
|
|
|
|
|
|
|
if not profile_id or not focus_area_id:
|
|
|
|
|
|
raise HTTPException(400, "profile_id und focus_area_id sind Pflichtfelder")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
INSERT INTO trainer_focus_areas (profile_id, focus_area_id, is_primary)
|
|
|
|
|
|
VALUES (%s, %s, %s)
|
|
|
|
|
|
ON CONFLICT (profile_id, focus_area_id) DO UPDATE
|
|
|
|
|
|
SET is_primary = EXCLUDED.is_primary
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
profile_id,
|
|
|
|
|
|
focus_area_id,
|
|
|
|
|
|
data.get('is_primary', False)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
tfa_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT tfa.*, fa.name as focus_area_name, p.name as trainer_name
|
|
|
|
|
|
FROM trainer_focus_areas tfa
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tfa.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN profiles p ON tfa.profile_id = p.id
|
|
|
|
|
|
WHERE tfa.id = %s
|
|
|
|
|
|
""", (tfa_id,))
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/trainer-focus-areas/{tfa_id}")
|
|
|
|
|
|
def delete_trainer_focus_area(tfa_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Remove focus area assignment (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Zuweisungen entfernen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
cur.execute("DELETE FROM trainer_focus_areas WHERE id = %s", (tfa_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
2026-04-23 08:55:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TARGET GROUPS (Zielgruppen)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/target-groups")
|
|
|
|
|
|
def list_target_groups(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
2026-04-23 09:27:13 +02:00
|
|
|
|
"""List all target groups (global catalog - independent of styles)."""
|
2026-04-23 08:55:54 +02:00
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
2026-04-23 09:27:13 +02:00
|
|
|
|
query = "SELECT * FROM target_groups"
|
2026-04-23 08:55:54 +02:00
|
|
|
|
params = []
|
|
|
|
|
|
where = []
|
|
|
|
|
|
|
2026-04-27 15:01:47 +02:00
|
|
|
|
frag, extra = _sql_active_status("status", status)
|
|
|
|
|
|
if frag:
|
|
|
|
|
|
where.append(frag.strip())
|
|
|
|
|
|
params.extend(extra)
|
2026-04-23 08:55:54 +02:00
|
|
|
|
|
|
|
|
|
|
if where:
|
|
|
|
|
|
query += " WHERE " + " AND ".join(where)
|
|
|
|
|
|
|
2026-04-23 09:27:13 +02:00
|
|
|
|
query += " ORDER BY sort_order, name"
|
2026-04-23 08:55:54 +02:00
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/target-groups")
|
|
|
|
|
|
def create_target_group(data: dict, session=Depends(require_auth)):
|
2026-04-23 09:27:13 +02:00
|
|
|
|
"""Create new target group (admin only, global catalog)."""
|
2026-04-23 08:55:54 +02:00
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Zielgruppen 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 target_groups (
|
2026-04-23 09:27:13 +02:00
|
|
|
|
name, description, min_age, max_age, sort_order, status
|
2026-04-23 08:55:54 +02:00
|
|
|
|
)
|
2026-04-23 09:27:13 +02:00
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
2026-04-23 08:55:54 +02:00
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
name,
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('min_age'),
|
|
|
|
|
|
data.get('max_age'),
|
|
|
|
|
|
data.get('sort_order', 99),
|
|
|
|
|
|
data.get('status', 'active')
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
target_group_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
2026-04-23 09:27:13 +02:00
|
|
|
|
cur.execute("SELECT * FROM target_groups WHERE id = %s", (target_group_id,))
|
2026-04-23 08:55:54 +02:00
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/target-groups/{target_group_id}")
|
|
|
|
|
|
def update_target_group(target_group_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update target group (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Zielgruppen bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
UPDATE target_groups SET
|
|
|
|
|
|
name = %s,
|
|
|
|
|
|
description = %s,
|
|
|
|
|
|
min_age = %s,
|
|
|
|
|
|
max_age = %s,
|
|
|
|
|
|
sort_order = %s,
|
|
|
|
|
|
status = %s,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('name'),
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('min_age'),
|
|
|
|
|
|
data.get('max_age'),
|
|
|
|
|
|
data.get('sort_order'),
|
|
|
|
|
|
data.get('status'),
|
|
|
|
|
|
target_group_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
2026-04-23 09:27:13 +02:00
|
|
|
|
cur.execute("SELECT * FROM target_groups WHERE id = %s", (target_group_id,))
|
2026-04-23 08:55:54 +02:00
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/target-groups/{target_group_id}")
|
|
|
|
|
|
def delete_target_group(target_group_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete target group (superadmin only).
|
|
|
|
|
|
|
2026-04-23 09:27:13 +02:00
|
|
|
|
Fails if target group is assigned to any exercises or training styles.
|
2026-04-23 08:55:54 +02:00
|
|
|
|
"""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role != 'superadmin':
|
|
|
|
|
|
raise HTTPException(403, "Nur Superadmins dürfen Zielgruppen löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Check if assigned to exercises
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT COUNT(*) as count
|
|
|
|
|
|
FROM exercise_target_groups
|
|
|
|
|
|
WHERE target_group_id = %s
|
|
|
|
|
|
""", (target_group_id,))
|
2026-04-23 09:27:13 +02:00
|
|
|
|
ex_count = cur.fetchone()['count']
|
|
|
|
|
|
|
|
|
|
|
|
# Check if assigned to training styles (M:N)
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT COUNT(*) as count
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_direction_target_groups
|
2026-04-23 09:27:13 +02:00
|
|
|
|
WHERE target_group_id = %s
|
|
|
|
|
|
""", (target_group_id,))
|
|
|
|
|
|
style_count = cur.fetchone()['count']
|
2026-04-23 08:55:54 +02:00
|
|
|
|
|
2026-04-23 09:27:13 +02:00
|
|
|
|
if ex_count > 0 or style_count > 0:
|
2026-04-23 08:55:54 +02:00
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
409,
|
2026-04-23 09:27:13 +02:00
|
|
|
|
f"Zielgruppe kann nicht gelöscht werden: {ex_count} Übung(en), {style_count} Stil(e) zugeordnet. "
|
|
|
|
|
|
"Bitte zuerst alle Zuordnungen entfernen."
|
2026-04-23 08:55:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("DELETE FROM target_groups WHERE id = %s", (target_group_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TRAINING STYLE → TARGET GROUPS (M:N Assignments)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/training-style-target-groups")
|
|
|
|
|
|
def list_training_style_target_groups(
|
2026-04-23 12:24:50 +02:00
|
|
|
|
style_direction_id: Optional[int] = Query(default=None),
|
2026-04-23 10:37:56 +02:00
|
|
|
|
target_group_id: Optional[int] = Query(default=None),
|
|
|
|
|
|
is_primary: Optional[bool] = Query(default=None),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
2026-04-23 12:24:50 +02:00
|
|
|
|
"""List M:N assignments between style directions and target groups.
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
Returns enriched data with style_direction_name, target_group_name,
|
2026-04-23 10:37:56 +02:00
|
|
|
|
focus_area_name for easy display in Matrix UI.
|
|
|
|
|
|
"""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
|
|
|
SELECT
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sdtg.id,
|
|
|
|
|
|
sdtg.style_direction_id,
|
|
|
|
|
|
sdtg.target_group_id,
|
|
|
|
|
|
sdtg.is_primary,
|
|
|
|
|
|
sdtg.created_at,
|
|
|
|
|
|
sd.name as style_direction_name,
|
|
|
|
|
|
sd.focus_area_id,
|
2026-04-23 10:37:56 +02:00
|
|
|
|
fa.name as focus_area_name,
|
|
|
|
|
|
tg.name as target_group_name,
|
|
|
|
|
|
tg.min_age,
|
|
|
|
|
|
tg.max_age
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_direction_target_groups sdtg
|
|
|
|
|
|
LEFT JOIN style_directions sd ON sdtg.style_direction_id = sd.id
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON sd.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN target_groups tg ON sdtg.target_group_id = tg.id
|
2026-04-23 10:37:56 +02:00
|
|
|
|
"""
|
|
|
|
|
|
params = []
|
|
|
|
|
|
where = []
|
|
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
if style_direction_id is not None:
|
|
|
|
|
|
where.append("sdtg.style_direction_id = %s")
|
|
|
|
|
|
params.append(style_direction_id)
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
if target_group_id is not None:
|
2026-04-23 12:24:50 +02:00
|
|
|
|
where.append("sdtg.target_group_id = %s")
|
2026-04-23 10:37:56 +02:00
|
|
|
|
params.append(target_group_id)
|
|
|
|
|
|
|
|
|
|
|
|
if is_primary is not None:
|
2026-04-23 12:24:50 +02:00
|
|
|
|
where.append("sdtg.is_primary = %s")
|
2026-04-23 10:37:56 +02:00
|
|
|
|
params.append(is_primary)
|
|
|
|
|
|
|
|
|
|
|
|
if where:
|
|
|
|
|
|
query += " WHERE " + " AND ".join(where)
|
|
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
query += " ORDER BY fa.sort_order, sd.sort_order, tg.sort_order"
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
cur.execute(query, params)
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/training-style-target-groups")
|
|
|
|
|
|
def create_training_style_target_group(data: dict, session=Depends(require_auth)):
|
2026-04-23 12:24:50 +02:00
|
|
|
|
"""Assign target group to style direction (admin only).
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
Uses UPSERT logic - if assignment exists, updates is_primary flag.
|
|
|
|
|
|
"""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Zuordnungen erstellen")
|
|
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
style_direction_id = data.get('style_direction_id')
|
2026-04-23 10:37:56 +02:00
|
|
|
|
target_group_id = data.get('target_group_id')
|
|
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
if not style_direction_id or not target_group_id:
|
|
|
|
|
|
raise HTTPException(400, "style_direction_id und target_group_id sind Pflichtfelder")
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Upsert logic
|
|
|
|
|
|
cur.execute("""
|
2026-04-23 12:24:50 +02:00
|
|
|
|
INSERT INTO style_direction_target_groups
|
|
|
|
|
|
(style_direction_id, target_group_id, is_primary)
|
2026-04-23 10:37:56 +02:00
|
|
|
|
VALUES (%s, %s, %s)
|
2026-04-23 12:24:50 +02:00
|
|
|
|
ON CONFLICT (style_direction_id, target_group_id)
|
2026-04-23 10:37:56 +02:00
|
|
|
|
DO UPDATE SET is_primary = EXCLUDED.is_primary
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
2026-04-23 12:24:50 +02:00
|
|
|
|
style_direction_id,
|
2026-04-23 10:37:56 +02:00
|
|
|
|
target_group_id,
|
|
|
|
|
|
data.get('is_primary', False)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
assignment_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
# Return enriched record
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sdtg.id,
|
|
|
|
|
|
sdtg.style_direction_id,
|
|
|
|
|
|
sdtg.target_group_id,
|
|
|
|
|
|
sdtg.is_primary,
|
|
|
|
|
|
sdtg.created_at,
|
|
|
|
|
|
sd.name as style_direction_name,
|
|
|
|
|
|
sd.focus_area_id,
|
2026-04-23 10:37:56 +02:00
|
|
|
|
fa.name as focus_area_name,
|
|
|
|
|
|
tg.name as target_group_name
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_direction_target_groups sdtg
|
|
|
|
|
|
LEFT JOIN style_directions sd ON sdtg.style_direction_id = sd.id
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON sd.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN target_groups tg ON sdtg.target_group_id = tg.id
|
|
|
|
|
|
WHERE sdtg.id = %s
|
2026-04-23 10:37:56 +02:00
|
|
|
|
""", (assignment_id,))
|
|
|
|
|
|
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/training-style-target-groups/{assignment_id}")
|
|
|
|
|
|
def update_training_style_target_group(
|
|
|
|
|
|
assignment_id: int,
|
|
|
|
|
|
data: dict,
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""Update M:N assignment (admin only).
|
|
|
|
|
|
|
|
|
|
|
|
Currently only supports updating is_primary flag.
|
|
|
|
|
|
"""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Zuordnungen bearbeiten")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
2026-04-23 12:24:50 +02:00
|
|
|
|
UPDATE style_direction_target_groups
|
2026-04-23 10:37:56 +02:00
|
|
|
|
SET is_primary = %s
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('is_primary', False),
|
|
|
|
|
|
assignment_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
# Return enriched record
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sdtg.id,
|
|
|
|
|
|
sdtg.style_direction_id,
|
|
|
|
|
|
sdtg.target_group_id,
|
|
|
|
|
|
sdtg.is_primary,
|
|
|
|
|
|
sdtg.created_at,
|
|
|
|
|
|
sd.name as style_direction_name,
|
|
|
|
|
|
sd.focus_area_id,
|
2026-04-23 10:37:56 +02:00
|
|
|
|
fa.name as focus_area_name,
|
|
|
|
|
|
tg.name as target_group_name
|
2026-04-23 12:24:50 +02:00
|
|
|
|
FROM style_direction_target_groups sdtg
|
|
|
|
|
|
LEFT JOIN style_directions sd ON sdtg.style_direction_id = sd.id
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON sd.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN target_groups tg ON sdtg.target_group_id = tg.id
|
|
|
|
|
|
WHERE sdtg.id = %s
|
2026-04-23 10:37:56 +02:00
|
|
|
|
""", (assignment_id,))
|
|
|
|
|
|
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/training-style-target-groups/{assignment_id}")
|
|
|
|
|
|
def delete_training_style_target_group(assignment_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Remove M:N assignment (admin only)."""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen Zuordnungen löschen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
2026-04-23 12:24:50 +02:00
|
|
|
|
cur.execute("DELETE FROM style_direction_target_groups WHERE id = %s", (assignment_id,))
|
2026-04-23 10:37:56 +02:00
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/training-styles/hierarchy")
|
|
|
|
|
|
def get_training_styles_hierarchy(
|
|
|
|
|
|
status: Optional[str] = Query(default='active'),
|
|
|
|
|
|
session=Depends(require_auth)
|
|
|
|
|
|
):
|
2026-04-23 12:24:50 +02:00
|
|
|
|
"""Get hierarchical structure: Focus Areas → Style Directions → Target Groups.
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
Returns nested structure for Tree-View rendering in Admin UI.
|
|
|
|
|
|
"""
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Get all focus areas
|
|
|
|
|
|
fa_query = "SELECT * FROM focus_areas"
|
|
|
|
|
|
fa_params = []
|
|
|
|
|
|
|
|
|
|
|
|
if status:
|
|
|
|
|
|
fa_query += " WHERE status = %s"
|
|
|
|
|
|
fa_params.append(status)
|
|
|
|
|
|
|
|
|
|
|
|
fa_query += " ORDER BY sort_order, name"
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute(fa_query, fa_params)
|
|
|
|
|
|
focus_areas = [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
# For each focus area, get style directions with their target groups
|
2026-04-23 10:37:56 +02:00
|
|
|
|
for fa in focus_areas:
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sd_query = """
|
|
|
|
|
|
SELECT * FROM style_directions
|
2026-04-23 10:37:56 +02:00
|
|
|
|
WHERE focus_area_id = %s
|
|
|
|
|
|
"""
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sd_params = [fa['id']]
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
if status:
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sd_query += " AND status = %s"
|
|
|
|
|
|
sd_params.append(status)
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sd_query += " ORDER BY sort_order, name"
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
cur.execute(sd_query, sd_params)
|
|
|
|
|
|
style_directions = [r2d(r) for r in cur.fetchall()]
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
# For each style direction, get assigned target groups
|
|
|
|
|
|
for sd in style_directions:
|
2026-04-23 10:37:56 +02:00
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
tg.id,
|
|
|
|
|
|
tg.name,
|
|
|
|
|
|
tg.description,
|
|
|
|
|
|
tg.min_age,
|
|
|
|
|
|
tg.max_age,
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sdtg.is_primary,
|
|
|
|
|
|
sdtg.id as assignment_id
|
|
|
|
|
|
FROM style_direction_target_groups sdtg
|
|
|
|
|
|
LEFT JOIN target_groups tg ON sdtg.target_group_id = tg.id
|
|
|
|
|
|
WHERE sdtg.style_direction_id = %s
|
2026-04-23 10:37:56 +02:00
|
|
|
|
ORDER BY tg.sort_order, tg.name
|
2026-04-23 12:24:50 +02:00
|
|
|
|
""", (sd['id'],))
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
sd['target_groups'] = [r2d(r) for r in cur.fetchall()]
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
2026-04-23 12:24:50 +02:00
|
|
|
|
fa['style_directions'] = style_directions
|
2026-04-23 10:37:56 +02:00
|
|
|
|
|
|
|
|
|
|
return focus_areas
|
2026-04-23 14:22:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TRAINER CONTEXTS (Fokussierte Trainer-Ansichten)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/trainer-contexts")
|
|
|
|
|
|
def list_trainer_contexts(session=Depends(require_auth)):
|
|
|
|
|
|
"""List all trainer contexts for the current user.
|
|
|
|
|
|
|
|
|
|
|
|
Returns enriched data with focus_area_name, style_direction_name, training_type_name.
|
|
|
|
|
|
"""
|
|
|
|
|
|
profile_id = session['profile_id']
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
tc.*,
|
|
|
|
|
|
fa.name as focus_area_name,
|
|
|
|
|
|
fa.icon as focus_area_icon,
|
|
|
|
|
|
sd.name as style_direction_name,
|
|
|
|
|
|
tt.name as training_type_name
|
|
|
|
|
|
FROM trainer_contexts tc
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tc.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN style_directions sd ON tc.style_direction_id = sd.id
|
|
|
|
|
|
LEFT JOIN training_types tt ON tc.training_type_id = tt.id
|
|
|
|
|
|
WHERE tc.profile_id = %s
|
|
|
|
|
|
ORDER BY tc.sort_order, tc.name
|
|
|
|
|
|
""", (profile_id,))
|
|
|
|
|
|
|
|
|
|
|
|
rows = cur.fetchall()
|
|
|
|
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/trainer-contexts")
|
|
|
|
|
|
def create_trainer_context(data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Create new trainer context for the current user."""
|
|
|
|
|
|
profile_id = session['profile_id']
|
|
|
|
|
|
|
|
|
|
|
|
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 trainer_contexts (
|
|
|
|
|
|
profile_id, name, focus_area_id, style_direction_id,
|
|
|
|
|
|
training_type_id, is_style_independent, description, sort_order, is_active
|
|
|
|
|
|
)
|
|
|
|
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
|
|
|
|
RETURNING id
|
|
|
|
|
|
""", (
|
|
|
|
|
|
profile_id,
|
|
|
|
|
|
name,
|
|
|
|
|
|
data.get('focus_area_id'),
|
|
|
|
|
|
data.get('style_direction_id'),
|
|
|
|
|
|
data.get('training_type_id'),
|
|
|
|
|
|
data.get('is_style_independent', False),
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('sort_order', 99),
|
|
|
|
|
|
data.get('is_active', True)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
context_id = cur.fetchone()['id']
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
# Return enriched record
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
tc.*,
|
|
|
|
|
|
fa.name as focus_area_name,
|
|
|
|
|
|
sd.name as style_direction_name,
|
|
|
|
|
|
tt.name as training_type_name
|
|
|
|
|
|
FROM trainer_contexts tc
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tc.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN style_directions sd ON tc.style_direction_id = sd.id
|
|
|
|
|
|
LEFT JOIN training_types tt ON tc.training_type_id = tt.id
|
|
|
|
|
|
WHERE tc.id = %s
|
|
|
|
|
|
""", (context_id,))
|
|
|
|
|
|
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/trainer-contexts/{context_id}")
|
|
|
|
|
|
def update_trainer_context(context_id: int, data: dict, session=Depends(require_auth)):
|
|
|
|
|
|
"""Update trainer context (own contexts only)."""
|
|
|
|
|
|
profile_id = session['profile_id']
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Verify ownership
|
|
|
|
|
|
cur.execute("SELECT profile_id FROM trainer_contexts WHERE id = %s", (context_id,))
|
|
|
|
|
|
row = cur.fetchone()
|
|
|
|
|
|
if not row:
|
|
|
|
|
|
raise HTTPException(404, "Kontext nicht gefunden")
|
|
|
|
|
|
if row['profile_id'] != profile_id:
|
|
|
|
|
|
raise HTTPException(403, "Zugriff verweigert")
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
UPDATE trainer_contexts SET
|
|
|
|
|
|
name = %s,
|
|
|
|
|
|
focus_area_id = %s,
|
|
|
|
|
|
style_direction_id = %s,
|
|
|
|
|
|
training_type_id = %s,
|
|
|
|
|
|
is_style_independent = %s,
|
|
|
|
|
|
description = %s,
|
|
|
|
|
|
sort_order = %s,
|
|
|
|
|
|
is_active = %s,
|
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
|
WHERE id = %s
|
|
|
|
|
|
""", (
|
|
|
|
|
|
data.get('name'),
|
|
|
|
|
|
data.get('focus_area_id'),
|
|
|
|
|
|
data.get('style_direction_id'),
|
|
|
|
|
|
data.get('training_type_id'),
|
|
|
|
|
|
data.get('is_style_independent', False),
|
|
|
|
|
|
data.get('description'),
|
|
|
|
|
|
data.get('sort_order'),
|
|
|
|
|
|
data.get('is_active', True),
|
|
|
|
|
|
context_id
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
# Return enriched record
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
tc.*,
|
|
|
|
|
|
fa.name as focus_area_name,
|
|
|
|
|
|
sd.name as style_direction_name,
|
|
|
|
|
|
tt.name as training_type_name
|
|
|
|
|
|
FROM trainer_contexts tc
|
|
|
|
|
|
LEFT JOIN focus_areas fa ON tc.focus_area_id = fa.id
|
|
|
|
|
|
LEFT JOIN style_directions sd ON tc.style_direction_id = sd.id
|
|
|
|
|
|
LEFT JOIN training_types tt ON tc.training_type_id = tt.id
|
|
|
|
|
|
WHERE tc.id = %s
|
|
|
|
|
|
""", (context_id,))
|
|
|
|
|
|
|
|
|
|
|
|
return r2d(cur.fetchone())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/trainer-contexts/{context_id}")
|
|
|
|
|
|
def delete_trainer_context(context_id: int, session=Depends(require_auth)):
|
|
|
|
|
|
"""Delete trainer context (own contexts only)."""
|
|
|
|
|
|
profile_id = session['profile_id']
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# Verify ownership
|
|
|
|
|
|
cur.execute("SELECT profile_id FROM trainer_contexts WHERE id = %s", (context_id,))
|
|
|
|
|
|
row = cur.fetchone()
|
|
|
|
|
|
if not row:
|
|
|
|
|
|
raise HTTPException(404, "Kontext nicht gefunden")
|
|
|
|
|
|
if row['profile_id'] != profile_id:
|
|
|
|
|
|
raise HTTPException(403, "Zugriff verweigert")
|
|
|
|
|
|
|
|
|
|
|
|
cur.execute("DELETE FROM trainer_contexts WHERE id = %s", (context_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return {"ok": True}
|
2026-04-24 07:51:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# HIERARCHICAL CATALOG VIEW (Admin UI Tree)
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/admin/hierarchy")
|
|
|
|
|
|
def get_admin_hierarchy(session=Depends(require_auth)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Get complete hierarchical catalog structure for admin tree view.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
[
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 1,
|
|
|
|
|
|
"name": "Karate",
|
|
|
|
|
|
"icon": "🥋",
|
|
|
|
|
|
"description": "...",
|
|
|
|
|
|
"style_directions": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 10,
|
|
|
|
|
|
"name": "Shotokan",
|
|
|
|
|
|
"abbreviation": "SKA",
|
|
|
|
|
|
"description": "...",
|
|
|
|
|
|
"target_groups": [
|
|
|
|
|
|
{"id": 100, "name": "Kinder", "is_primary": true}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"training_types": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": 20,
|
|
|
|
|
|
"name": "Breitensport",
|
|
|
|
|
|
"abbreviation": "BS",
|
|
|
|
|
|
"description": "..."
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
"""
|
|
|
|
|
|
role = session.get('role')
|
|
|
|
|
|
if role not in ['admin', 'superadmin']:
|
|
|
|
|
|
raise HTTPException(403, "Nur Admins dürfen die Hierarchie abrufen")
|
|
|
|
|
|
|
|
|
|
|
|
with get_db() as conn:
|
|
|
|
|
|
cur = get_cursor(conn)
|
|
|
|
|
|
|
|
|
|
|
|
# 1. Get all focus areas
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT id, name, icon, description, sort_order, status
|
|
|
|
|
|
FROM focus_areas
|
|
|
|
|
|
WHERE status = 'active'
|
|
|
|
|
|
ORDER BY sort_order, name
|
|
|
|
|
|
""")
|
|
|
|
|
|
focus_areas = [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Get all style directions with their target group assignments
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
sd.id,
|
|
|
|
|
|
sd.name,
|
|
|
|
|
|
sd.abbreviation,
|
|
|
|
|
|
sd.description,
|
|
|
|
|
|
sd.focus_area_id,
|
|
|
|
|
|
sd.sort_order,
|
|
|
|
|
|
json_agg(
|
|
|
|
|
|
json_build_object(
|
|
|
|
|
|
'id', tg.id,
|
|
|
|
|
|
'name', tg.name,
|
|
|
|
|
|
'is_primary', sdtg.is_primary
|
|
|
|
|
|
) ORDER BY tg.name
|
|
|
|
|
|
) FILTER (WHERE tg.id IS NOT NULL) as target_groups
|
|
|
|
|
|
FROM style_directions sd
|
|
|
|
|
|
LEFT JOIN style_direction_target_groups sdtg ON sd.id = sdtg.style_direction_id
|
|
|
|
|
|
LEFT JOIN target_groups tg ON sdtg.target_group_id = tg.id
|
|
|
|
|
|
WHERE sd.status = 'active'
|
|
|
|
|
|
GROUP BY sd.id, sd.name, sd.abbreviation, sd.description, sd.focus_area_id, sd.sort_order
|
|
|
|
|
|
ORDER BY sd.sort_order, sd.name
|
|
|
|
|
|
""")
|
|
|
|
|
|
style_directions = [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
# 3. Get all training types
|
|
|
|
|
|
cur.execute("""
|
|
|
|
|
|
SELECT id, name, abbreviation, description, focus_area_id, sort_order
|
|
|
|
|
|
FROM training_types
|
|
|
|
|
|
WHERE status = 'active'
|
|
|
|
|
|
ORDER BY sort_order, name
|
|
|
|
|
|
""")
|
|
|
|
|
|
training_types = [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
# 4. Build hierarchy
|
|
|
|
|
|
for fa in focus_areas:
|
|
|
|
|
|
fa_id = fa['id']
|
|
|
|
|
|
|
|
|
|
|
|
# Attach style directions
|
|
|
|
|
|
fa['style_directions'] = [
|
|
|
|
|
|
sd for sd in style_directions
|
|
|
|
|
|
if sd['focus_area_id'] == fa_id
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# Attach training types
|
|
|
|
|
|
fa['training_types'] = [
|
|
|
|
|
|
tt for tt in training_types
|
|
|
|
|
|
if tt['focus_area_id'] == fa_id
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
return focus_areas
|