feat: Backend API für training_types + Frontend api.js
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:
parent
62b5b4c2fd
commit
72c927e69e
|
|
@ -359,6 +359,131 @@ def delete_training_character(char_id: int, session=Depends(require_auth)):
|
||||||
return {"ok": True}
|
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
|
# SKILL CATEGORIES
|
||||||
# ════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,30 @@ export async function deleteTrainingCharacter(id) {
|
||||||
return request(`/api/training-characters/${id}`, { method: 'DELETE' })
|
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
|
// Skill Categories
|
||||||
export async function listSkillCategories(filters = {}) {
|
export async function listSkillCategories(filters = {}) {
|
||||||
const query = new URLSearchParams(filters).toString()
|
const query = new URLSearchParams(filters).toString()
|
||||||
|
|
@ -512,6 +536,10 @@ export const api = {
|
||||||
createTrainingCharacter,
|
createTrainingCharacter,
|
||||||
updateTrainingCharacter,
|
updateTrainingCharacter,
|
||||||
deleteTrainingCharacter,
|
deleteTrainingCharacter,
|
||||||
|
listTrainingTypes,
|
||||||
|
createTrainingType,
|
||||||
|
updateTrainingType,
|
||||||
|
deleteTrainingType,
|
||||||
listSkillCategories,
|
listSkillCategories,
|
||||||
createSkillCategory,
|
createSkillCategory,
|
||||||
updateSkillCategory,
|
updateSkillCategory,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Shinkan Jinkendo Frontend Version
|
// 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 BUILD_DATE = "2026-04-23"
|
||||||
|
|
||||||
export const PAGE_VERSIONS = {
|
export const PAGE_VERSIONS = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user