Backend:
- GET /api/target-groups: Liste mit hierarchischem Kontext (focus_area → training_style → target_group)
- POST /api/target-groups: Create (admin only)
- PUT /api/target-groups/{id}: Update (admin only)
- DELETE /api/target-groups/{id}: Delete (superadmin only) mit CASCADE-Schutz
- Filter: by training_style_id, status
Frontend:
- api.js: listTargetGroups, createTargetGroup, updateTargetGroup, deleteTargetGroup
- AdminCatalogsPage: Neuer Tab "Zielgruppen" (6. Tab)
- Create-Form: training_style_id, name, description, min_age, max_age
- List-View: Hierarchie-Anzeige (Fokusbereich → Stil → Zielgruppe + Altersbereich)
- Inline-Editing mit Stil-Auswahl-Dropdown
- Delete mit Confirmation Dialog
Architektur:
- Hierarchische Beziehung: target_groups.training_style_id → training_styles → focus_areas
- CASCADE-Protection: DELETE verweigert wenn exercise_target_groups Einträge existieren
- Backend liefert enriched data mit training_style_name + focus_area_name
version: 0.3.2
modules: catalogs 1.2.0
pages: AdminCatalogsPage 1.1.0
750 lines
25 KiB
Python
750 lines
25 KiB
Python
"""
|
|
Catalog Management Endpoints for Shinkan Jinkendo
|
|
|
|
Admin-verwaltbare Stammdaten für Übungen, Fokusbereiche, Stile, etc.
|
|
"""
|
|
from typing import Optional
|
|
from fastapi import APIRouter, HTTPException, Depends, Query
|
|
|
|
from db import get_db, get_cursor, r2d
|
|
from auth import require_auth
|
|
|
|
router = APIRouter(prefix="/api", tags=["catalogs"])
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# 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 = []
|
|
|
|
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("/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 = """
|
|
SELECT ts.*, ps.name as parent_style_name
|
|
FROM training_styles ts
|
|
LEFT JOIN training_styles ps ON ts.parent_style_id = ps.id
|
|
"""
|
|
params = []
|
|
|
|
if status:
|
|
query += " WHERE ts.status = %s"
|
|
params.append(status)
|
|
|
|
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("""
|
|
INSERT INTO training_styles (name, abbreviation, description, parent_style_id, sort_order, status)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
RETURNING id
|
|
""", (
|
|
name,
|
|
data.get('abbreviation'),
|
|
data.get('description'),
|
|
data.get('parent_style_id'),
|
|
data.get('sort_order', 99),
|
|
data.get('status', 'active')
|
|
))
|
|
|
|
style_id = cur.fetchone()['id']
|
|
conn.commit()
|
|
|
|
cur.execute("""
|
|
SELECT ts.*, ps.name as parent_style_name
|
|
FROM training_styles ts
|
|
LEFT JOIN training_styles ps ON ts.parent_style_id = ps.id
|
|
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("""
|
|
UPDATE training_styles SET
|
|
name = %s,
|
|
abbreviation = %s,
|
|
description = %s,
|
|
parent_style_id = %s,
|
|
sort_order = %s,
|
|
status = %s,
|
|
updated_at = NOW()
|
|
WHERE id = %s
|
|
""", (
|
|
data.get('name'),
|
|
data.get('abbreviation'),
|
|
data.get('description'),
|
|
data.get('parent_style_id'),
|
|
data.get('sort_order'),
|
|
data.get('status'),
|
|
style_id
|
|
))
|
|
|
|
conn.commit()
|
|
|
|
cur.execute("""
|
|
SELECT ts.*, ps.name as parent_style_name
|
|
FROM training_styles ts
|
|
LEFT JOIN training_styles ps ON ts.parent_style_id = ps.id
|
|
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)
|
|
cur.execute("DELETE FROM training_styles WHERE id = %s", (style_id,))
|
|
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}
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# SKILL CATEGORIES
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
@router.get("/skill-categories")
|
|
def list_skill_categories(
|
|
status: Optional[str] = Query(default='active'),
|
|
session=Depends(require_auth)
|
|
):
|
|
"""List all skill categories."""
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
query = """
|
|
SELECT sc.*, pc.name as parent_category_name
|
|
FROM skill_categories sc
|
|
LEFT JOIN skill_categories pc ON sc.parent_category_id = pc.id
|
|
"""
|
|
params = []
|
|
|
|
if status:
|
|
query += " WHERE sc.status = %s"
|
|
params.append(status)
|
|
|
|
query += " ORDER BY sc.sort_order, sc.name"
|
|
|
|
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")
|
|
|
|
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 skill_categories (name, description, parent_category_id, sort_order, status)
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
RETURNING id
|
|
""", (
|
|
name,
|
|
data.get('description'),
|
|
data.get('parent_category_id'),
|
|
data.get('sort_order', 99),
|
|
data.get('status', 'active')
|
|
))
|
|
|
|
cat_id = cur.fetchone()['id']
|
|
conn.commit()
|
|
|
|
cur.execute("""
|
|
SELECT sc.*, pc.name as parent_category_name
|
|
FROM skill_categories sc
|
|
LEFT JOIN skill_categories pc ON sc.parent_category_id = pc.id
|
|
WHERE sc.id = %s
|
|
""", (cat_id,))
|
|
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)
|
|
|
|
cur.execute("""
|
|
UPDATE skill_categories SET
|
|
name = %s,
|
|
description = %s,
|
|
parent_category_id = %s,
|
|
sort_order = %s,
|
|
status = %s,
|
|
updated_at = NOW()
|
|
WHERE id = %s
|
|
""", (
|
|
data.get('name'),
|
|
data.get('description'),
|
|
data.get('parent_category_id'),
|
|
data.get('sort_order'),
|
|
data.get('status'),
|
|
cat_id
|
|
))
|
|
|
|
conn.commit()
|
|
|
|
cur.execute("""
|
|
SELECT sc.*, pc.name as parent_category_name
|
|
FROM skill_categories sc
|
|
LEFT JOIN skill_categories pc ON sc.parent_category_id = pc.id
|
|
WHERE sc.id = %s
|
|
""", (cat_id,))
|
|
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)
|
|
cur.execute("DELETE FROM skill_categories WHERE id = %s", (cat_id,))
|
|
conn.commit()
|
|
|
|
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}
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# TARGET GROUPS (Zielgruppen)
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
@router.get("/target-groups")
|
|
def list_target_groups(
|
|
status: Optional[str] = Query(default='active'),
|
|
training_style_id: Optional[int] = Query(default=None),
|
|
session=Depends(require_auth)
|
|
):
|
|
"""List all target groups with hierarchical context."""
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
query = """
|
|
SELECT tg.*,
|
|
ts.name as training_style_name,
|
|
fa.name as focus_area_name
|
|
FROM target_groups tg
|
|
LEFT JOIN training_styles ts ON tg.training_style_id = ts.id
|
|
LEFT JOIN focus_areas fa ON ts.focus_area_id = fa.id
|
|
"""
|
|
params = []
|
|
where = []
|
|
|
|
if status:
|
|
where.append("tg.status = %s")
|
|
params.append(status)
|
|
|
|
if training_style_id:
|
|
where.append("tg.training_style_id = %s")
|
|
params.append(training_style_id)
|
|
|
|
if where:
|
|
query += " WHERE " + " AND ".join(where)
|
|
|
|
query += " ORDER BY fa.name, ts.name, tg.sort_order, tg.name"
|
|
|
|
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)):
|
|
"""Create new target group (admin only)."""
|
|
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 (
|
|
training_style_id, name, description,
|
|
min_age, max_age, sort_order, status
|
|
)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
RETURNING id
|
|
""", (
|
|
data.get('training_style_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()
|
|
|
|
# Return with hierarchical context
|
|
cur.execute("""
|
|
SELECT tg.*,
|
|
ts.name as training_style_name,
|
|
fa.name as focus_area_name
|
|
FROM target_groups tg
|
|
LEFT JOIN training_styles ts ON tg.training_style_id = ts.id
|
|
LEFT JOIN focus_areas fa ON ts.focus_area_id = fa.id
|
|
WHERE tg.id = %s
|
|
""", (target_group_id,))
|
|
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
|
|
training_style_id = %s,
|
|
name = %s,
|
|
description = %s,
|
|
min_age = %s,
|
|
max_age = %s,
|
|
sort_order = %s,
|
|
status = %s,
|
|
updated_at = NOW()
|
|
WHERE id = %s
|
|
""", (
|
|
data.get('training_style_id'),
|
|
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()
|
|
|
|
# Return with hierarchical context
|
|
cur.execute("""
|
|
SELECT tg.*,
|
|
ts.name as training_style_name,
|
|
fa.name as focus_area_name
|
|
FROM target_groups tg
|
|
LEFT JOIN training_styles ts ON tg.training_style_id = ts.id
|
|
LEFT JOIN focus_areas fa ON ts.focus_area_id = fa.id
|
|
WHERE tg.id = %s
|
|
""", (target_group_id,))
|
|
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).
|
|
|
|
Fails if target group is assigned to any exercises (CASCADE protection).
|
|
"""
|
|
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,))
|
|
count = cur.fetchone()['count']
|
|
|
|
if count > 0:
|
|
raise HTTPException(
|
|
409,
|
|
f"Zielgruppe kann nicht gelöscht werden: {count} Übung(en) zugeordnet. "
|
|
"Bitte zuerst alle Zuordnungen entfernen oder umrouten."
|
|
)
|
|
|
|
cur.execute("DELETE FROM target_groups WHERE id = %s", (target_group_id,))
|
|
conn.commit()
|
|
|
|
return {"ok": True}
|