""" 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}