""" Admin: training_category_parameter + training_type_parameter (attribute profiles). Agent guide: .claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md """ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from auth import require_admin from db import get_db, get_cursor, r2d router = APIRouter(prefix="/api/admin", tags=["admin", "activity-attribute-profiles"]) class CategoryParameterCreate(BaseModel): training_category: str = Field(..., min_length=1, max_length=50) training_parameter_id: int sort_order: int = 0 required: bool = False ui_group: Optional[str] = Field(None, max_length=50) class TypeParameterCreate(BaseModel): training_type_id: int training_parameter_id: int sort_order: Optional[int] = None required: Optional[bool] = None ui_group: Optional[str] = Field(None, max_length=50) class CategoryParameterUpdate(BaseModel): sort_order: Optional[int] = None required: Optional[bool] = None ui_group: Optional[str] = Field(None, max_length=50) class TypeParameterUpdate(BaseModel): sort_order: Optional[int] = None required: Optional[bool] = None ui_group: Optional[str] = Field(None, max_length=50) @router.get("/training-category-parameters") def admin_list_category_parameters( category: Optional[str] = Query(None, description="Filter: training_types.category"), session: dict = Depends(require_admin), ): with get_db() as conn: cur = get_cursor(conn) if category: cur.execute( """ SELECT tcp.*, tp.key AS parameter_key, tp.name_de AS parameter_name_de FROM training_category_parameter tcp JOIN training_parameters tp ON tp.id = tcp.training_parameter_id WHERE tcp.training_category = %s ORDER BY tcp.sort_order, tp.key """, (category.strip(),), ) else: cur.execute( """ SELECT tcp.*, tp.key AS parameter_key, tp.name_de AS parameter_name_de FROM training_category_parameter tcp JOIN training_parameters tp ON tp.id = tcp.training_parameter_id ORDER BY tcp.training_category, tcp.sort_order, tp.key """ ) return [r2d(r) for r in cur.fetchall()] @router.post("/training-category-parameters") def admin_add_category_parameter( body: CategoryParameterCreate, session: dict = Depends(require_admin), ): cat = body.training_category.strip() with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT id FROM training_parameters WHERE id = %s", (body.training_parameter_id,)) if not cur.fetchone(): raise HTTPException(404, "training_parameter_id unbekannt") try: cur.execute( """ INSERT INTO training_category_parameter ( training_category, training_parameter_id, sort_order, required, ui_group ) VALUES (%s,%s,%s,%s,%s) RETURNING id """, (cat, body.training_parameter_id, body.sort_order, body.required, body.ui_group), ) new_id = cur.fetchone()["id"] conn.commit() except Exception as e: conn.rollback() if "uq_training_category_parameter" in str(e).lower() or "unique" in str(e).lower(): raise HTTPException(409, "Zuordnung existiert bereits") from e raise HTTPException(400, str(e)) from e return {"id": new_id} @router.put("/training-category-parameters/{link_id}") def admin_update_category_parameter( link_id: int, body: CategoryParameterUpdate, session: dict = Depends(require_admin), ): patch = body.model_dump(exclude_unset=True) if not patch: raise HTTPException(400, "Keine Felder zum Aktualisieren") cols: list[str] = [] vals: list = [] if "sort_order" in patch: cols.append("sort_order = %s") vals.append(patch["sort_order"]) if "required" in patch: cols.append("required = %s") vals.append(patch["required"]) if "ui_group" in patch: cols.append("ui_group = %s") vals.append(patch["ui_group"].strip() if patch["ui_group"] else None) if not cols: raise HTTPException(400, "Keine Felder zum Aktualisieren") vals.append(link_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( f"UPDATE training_category_parameter SET {', '.join(cols)} WHERE id = %s RETURNING id", vals, ) if not cur.fetchone(): raise HTTPException(404, "Eintrag nicht gefunden") conn.commit() return {"ok": True, "id": link_id} @router.delete("/training-category-parameters/{link_id}") def admin_delete_category_parameter( link_id: int, session: dict = Depends(require_admin), ): with get_db() as conn: cur = get_cursor(conn) cur.execute( "DELETE FROM training_category_parameter WHERE id = %s RETURNING id", (link_id,), ) if not cur.fetchone(): raise HTTPException(404, "Eintrag nicht gefunden") conn.commit() return {"ok": True} @router.get("/training-type-parameters") def admin_list_type_parameters( training_type_id: int = Query(..., ge=1), session: dict = Depends(require_admin), ): with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT ttp.*, tp.key AS parameter_key, tp.name_de AS parameter_name_de FROM training_type_parameter ttp JOIN training_parameters tp ON tp.id = ttp.training_parameter_id WHERE ttp.training_type_id = %s ORDER BY ttp.sort_order NULLS LAST, tp.key """, (training_type_id,), ) return [r2d(r) for r in cur.fetchall()] @router.post("/training-type-parameters") def admin_add_type_parameter( body: TypeParameterCreate, session: dict = Depends(require_admin), ): with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT id FROM training_types WHERE id = %s", (body.training_type_id,)) if not cur.fetchone(): raise HTTPException(404, "training_type_id unbekannt") cur.execute("SELECT id FROM training_parameters WHERE id = %s", (body.training_parameter_id,)) if not cur.fetchone(): raise HTTPException(404, "training_parameter_id unbekannt") try: cur.execute( """ INSERT INTO training_type_parameter ( training_type_id, training_parameter_id, sort_order, required, ui_group ) VALUES (%s,%s,%s,%s,%s) RETURNING id """, ( body.training_type_id, body.training_parameter_id, body.sort_order, body.required, body.ui_group, ), ) new_id = cur.fetchone()["id"] conn.commit() except Exception as e: conn.rollback() if "uq_training_type_parameter" in str(e).lower() or "unique" in str(e).lower(): raise HTTPException(409, "Zuordnung existiert bereits") from e raise HTTPException(400, str(e)) from e return {"id": new_id} @router.put("/training-type-parameters/{link_id}") def admin_update_type_parameter( link_id: int, body: TypeParameterUpdate, session: dict = Depends(require_admin), ): patch = body.model_dump(exclude_unset=True) if not patch: raise HTTPException(400, "Keine Felder zum Aktualisieren") cols: list[str] = [] vals: list = [] if "sort_order" in patch: cols.append("sort_order = %s") vals.append(patch["sort_order"]) if "required" in patch: cols.append("required = %s") vals.append(patch["required"]) if "ui_group" in patch: cols.append("ui_group = %s") vals.append(patch["ui_group"].strip() if patch["ui_group"] else None) if not cols: raise HTTPException(400, "Keine Felder zum Aktualisieren") vals.append(link_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( f"UPDATE training_type_parameter SET {', '.join(cols)} WHERE id = %s RETURNING id", vals, ) if not cur.fetchone(): raise HTTPException(404, "Eintrag nicht gefunden") conn.commit() return {"ok": True, "id": link_id} @router.delete("/training-type-parameters/{link_id}") def admin_delete_type_parameter( link_id: int, session: dict = Depends(require_admin), ): with get_db() as conn: cur = get_cursor(conn) cur.execute( "DELETE FROM training_type_parameter WHERE id = %s RETURNING id", (link_id,), ) if not cur.fetchone(): raise HTTPException(404, "Eintrag nicht gefunden") conn.commit() return {"ok": True}