feat: Backend API für training_types + Frontend api.js
Some checks failed
Deploy Development / deploy (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 1m55s

Backend (catalogs.py):
- GET /api/training-types (Liste)
- POST /api/training-types (Erstellen)
- PUT /api/training-types/{id} (Bearbeiten)
- DELETE /api/training-types/{id} (Löschen mit CASCADE-Check)
- Cascade-Protection: Fehler wenn Übungen zugeordnet

Frontend (api.js):
- listTrainingTypes(filters)
- createTrainingType(data)
- updateTrainingType(id, data)
- deleteTrainingType(id)
- Export zum api-Objekt hinzugefügt

Pattern: Konsistent mit anderen Katalog-Endpoints
CRUD: Volle Admin-Verwaltung

Version: 0.4.0
This commit is contained in:
Lars 2026-04-23 12:12:48 +02:00
parent 62b5b4c2fd
commit 72c927e69e
3 changed files with 154 additions and 1 deletions

View File

@ -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
# ════════════════════════════════════════════════════════════════════════

View File

@ -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,

View File

@ -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 = {