diff --git a/backend/routers/catalogs.py b/backend/routers/catalogs.py index d4e7a76..866e305 100644 --- a/backend/routers/catalogs.py +++ b/backend/routers/catalogs.py @@ -359,6 +359,131 @@ def delete_training_character(char_id: int, session=Depends(require_auth)): return {"ok": True} +# ════════════════════════════════════════════════════════════════════════ +# TRAINING TYPES (Breitensport, Leistungssport, etc.) +# ════════════════════════════════════════════════════════════════════════ + +@router.get("/training-types") +def list_training_types( + status: Optional[str] = Query(default='active'), + session=Depends(require_auth) +): + """List all training types.""" + with get_db() as conn: + cur = get_cursor(conn) + + query = "SELECT * FROM training_types" + 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-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(""" + INSERT INTO training_types (name, abbreviation, description, sort_order, status) + VALUES (%s, %s, %s, %s, %s) + RETURNING id + """, ( + name, + data.get('abbreviation'), + data.get('description'), + data.get('sort_order', 99), + data.get('status', 'active') + )) + + type_id = cur.fetchone()['id'] + conn.commit() + + cur.execute("SELECT * FROM training_types WHERE id = %s", (type_id,)) + 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, + sort_order = %s, + status = %s, + updated_at = NOW() + WHERE id = %s + """, ( + data.get('name'), + data.get('abbreviation'), + data.get('description'), + data.get('sort_order'), + data.get('status'), + type_id + )) + + conn.commit() + + cur.execute("SELECT * FROM training_types WHERE id = %s", (type_id,)) + 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} + + # ════════════════════════════════════════════════════════════════════════ # SKILL CATEGORIES # ════════════════════════════════════════════════════════════════════════ diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 16c9e07..5b2f89f 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -305,6 +305,30 @@ export async function deleteTrainingCharacter(id) { return request(`/api/training-characters/${id}`, { method: 'DELETE' }) } +// Training Types (Breitensport, Leistungssport, etc.) +export async function listTrainingTypes(filters = {}) { + const query = new URLSearchParams(filters).toString() + return request(`/api/training-types${query ? '?' + query : ''}`) +} + +export async function createTrainingType(data) { + return request('/api/training-types', { + method: 'POST', + body: JSON.stringify(data) + }) +} + +export async function updateTrainingType(id, data) { + return request(`/api/training-types/${id}`, { + method: 'PUT', + body: JSON.stringify(data) + }) +} + +export async function deleteTrainingType(id) { + return request(`/api/training-types/${id}`, { method: 'DELETE' }) +} + // Skill Categories export async function listSkillCategories(filters = {}) { const query = new URLSearchParams(filters).toString() @@ -512,6 +536,10 @@ export const api = { createTrainingCharacter, updateTrainingCharacter, deleteTrainingCharacter, + listTrainingTypes, + createTrainingType, + updateTrainingType, + deleteTrainingType, listSkillCategories, createSkillCategory, updateSkillCategory, diff --git a/frontend/src/version.js b/frontend/src/version.js index 9f2300b..32c44e8 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -1,6 +1,6 @@ // Shinkan Jinkendo Frontend Version -export const APP_VERSION = "0.3.4" +export const APP_VERSION = "0.4.0" export const BUILD_DATE = "2026-04-23" export const PAGE_VERSIONS = {