Backend: - Created routers/skills.py with full CRUD - Skills: list (with category filter), get, create, update, delete - Methods: list (with category filter), get, create, update, delete - Default: only show active items - Read access: all authenticated users - Write access: admin only - Registered skills router in main.py Frontend: - Complete SkillsPage with 2 tabs (Fähigkeiten, Trainingsmethoden) - Browse by category with cards layout - Admin CRUD forms (importance rating for skills, duration/group size for methods) - Mobile-responsive grid layout - Updated api.js with all skill/method functions - Added /skills route to App.jsx Migration already exists: 003_catalogs.sql (skills, training_methods + seed data) Next: Training Planning (core feature)
341 lines
11 KiB
Python
341 lines
11 KiB
Python
"""
|
|
Skills & Methods Catalog Endpoints for Shinkan Jinkendo
|
|
|
|
Handles CRUD operations for skills and training methods.
|
|
Read access for all authenticated users.
|
|
Write access for admins only.
|
|
"""
|
|
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=["skills"])
|
|
|
|
|
|
# ── List Skills ───────────────────────────────────────────────────────
|
|
@router.get("/skills")
|
|
def list_skills(
|
|
category: Optional[str] = Query(default=None),
|
|
status: Optional[str] = Query(default=None),
|
|
session=Depends(require_auth)
|
|
):
|
|
"""
|
|
List all skills (public for authenticated users).
|
|
|
|
Filters:
|
|
- category: kihon, kumite, kata, selbstverteidigung, fitness
|
|
- status: active, inactive
|
|
"""
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
query = "SELECT * FROM skills"
|
|
params = []
|
|
where = []
|
|
|
|
if category:
|
|
where.append("category = %s")
|
|
params.append(category)
|
|
|
|
if status:
|
|
where.append("status = %s")
|
|
params.append(status)
|
|
else:
|
|
# Default: only active skills
|
|
where.append("status = 'active'")
|
|
|
|
if where:
|
|
query += " WHERE " + " AND ".join(where)
|
|
|
|
query += " ORDER BY importance DESC, name"
|
|
|
|
cur.execute(query, params)
|
|
rows = cur.fetchall()
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
# ── Get Skill ─────────────────────────────────────────────────────────
|
|
@router.get("/skills/{skill_id}")
|
|
def get_skill(skill_id: int, session=Depends(require_auth)):
|
|
"""Get skill by ID."""
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
cur.execute("SELECT * FROM skills WHERE id = %s", (skill_id,))
|
|
skill = cur.fetchone()
|
|
|
|
if not skill:
|
|
raise HTTPException(404, "Fähigkeit nicht gefunden")
|
|
|
|
return r2d(skill)
|
|
|
|
|
|
# ── Create Skill ──────────────────────────────────────────────────────
|
|
@router.post("/skills")
|
|
def create_skill(data: dict, session=Depends(require_auth)):
|
|
"""Create new skill (admin only)."""
|
|
role = session.get('role')
|
|
if role not in ['admin', 'superadmin']:
|
|
raise HTTPException(403, "Nur Admins dürfen Fähigkeiten 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 skills (name, category, description, importance, keywords, status)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
RETURNING id
|
|
""", (
|
|
name,
|
|
data.get('category'),
|
|
data.get('description'),
|
|
data.get('importance'),
|
|
data.get('keywords'),
|
|
data.get('status', 'active')
|
|
))
|
|
|
|
skill_id = cur.fetchone()['id']
|
|
conn.commit()
|
|
|
|
return get_skill(skill_id, session)
|
|
|
|
|
|
# ── Update Skill ──────────────────────────────────────────────────────
|
|
@router.put("/skills/{skill_id}")
|
|
def update_skill(skill_id: int, data: dict, session=Depends(require_auth)):
|
|
"""Update skill (admin only)."""
|
|
role = session.get('role')
|
|
if role not in ['admin', 'superadmin']:
|
|
raise HTTPException(403, "Nur Admins dürfen Fähigkeiten bearbeiten")
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Check existence
|
|
cur.execute("SELECT id FROM skills WHERE id = %s", (skill_id,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "Fähigkeit nicht gefunden")
|
|
|
|
# Update
|
|
cur.execute("""
|
|
UPDATE skills SET
|
|
name = %s,
|
|
category = %s,
|
|
description = %s,
|
|
importance = %s,
|
|
keywords = %s,
|
|
status = %s,
|
|
updated_at = NOW()
|
|
WHERE id = %s
|
|
""", (
|
|
data.get('name'),
|
|
data.get('category'),
|
|
data.get('description'),
|
|
data.get('importance'),
|
|
data.get('keywords'),
|
|
data.get('status'),
|
|
skill_id
|
|
))
|
|
|
|
conn.commit()
|
|
|
|
return get_skill(skill_id, session)
|
|
|
|
|
|
# ── Delete Skill ──────────────────────────────────────────────────────
|
|
@router.delete("/skills/{skill_id}")
|
|
def delete_skill(skill_id: int, session=Depends(require_auth)):
|
|
"""Delete skill (superadmin only)."""
|
|
role = session.get('role')
|
|
if role != 'superadmin':
|
|
raise HTTPException(403, "Nur Superadmins dürfen Fähigkeiten löschen")
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Check existence
|
|
cur.execute("SELECT id FROM skills WHERE id = %s", (skill_id,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "Fähigkeit nicht gefunden")
|
|
|
|
# Delete
|
|
cur.execute("DELETE FROM skills WHERE id = %s", (skill_id,))
|
|
conn.commit()
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
# ── List Training Methods ─────────────────────────────────────────────
|
|
@router.get("/methods")
|
|
def list_methods(
|
|
category: Optional[str] = Query(default=None),
|
|
status: Optional[str] = Query(default=None),
|
|
session=Depends(require_auth)
|
|
):
|
|
"""
|
|
List all training methods (public for authenticated users).
|
|
|
|
Filters:
|
|
- category: kondition, didaktik, koordination, kraft, etc.
|
|
- status: active, inactive
|
|
"""
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
query = "SELECT * FROM training_methods"
|
|
params = []
|
|
where = []
|
|
|
|
if category:
|
|
where.append("category = %s")
|
|
params.append(category)
|
|
|
|
if status:
|
|
where.append("status = %s")
|
|
params.append(status)
|
|
else:
|
|
# Default: only active methods
|
|
where.append("status = 'active'")
|
|
|
|
if where:
|
|
query += " WHERE " + " AND ".join(where)
|
|
|
|
query += " ORDER BY name"
|
|
|
|
cur.execute(query, params)
|
|
rows = cur.fetchall()
|
|
return [r2d(r) for r in rows]
|
|
|
|
|
|
# ── Get Training Method ───────────────────────────────────────────────
|
|
@router.get("/methods/{method_id}")
|
|
def get_method(method_id: int, session=Depends(require_auth)):
|
|
"""Get training method by ID."""
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
cur.execute("SELECT * FROM training_methods WHERE id = %s", (method_id,))
|
|
method = cur.fetchone()
|
|
|
|
if not method:
|
|
raise HTTPException(404, "Trainingsmethode nicht gefunden")
|
|
|
|
return r2d(method)
|
|
|
|
|
|
# ── Create Training Method ────────────────────────────────────────────
|
|
@router.post("/methods")
|
|
def create_method(data: dict, session=Depends(require_auth)):
|
|
"""Create new training method (admin only)."""
|
|
role = session.get('role')
|
|
if role not in ['admin', 'superadmin']:
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingsmethoden 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_methods (
|
|
name, abbreviation, category, description,
|
|
typical_duration, typical_group_size,
|
|
related_skills, keywords, status
|
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
|
RETURNING id
|
|
""", (
|
|
name,
|
|
data.get('abbreviation'),
|
|
data.get('category'),
|
|
data.get('description'),
|
|
data.get('typical_duration'),
|
|
data.get('typical_group_size'),
|
|
data.get('related_skills'),
|
|
data.get('keywords'),
|
|
data.get('status', 'active')
|
|
))
|
|
|
|
method_id = cur.fetchone()['id']
|
|
conn.commit()
|
|
|
|
return get_method(method_id, session)
|
|
|
|
|
|
# ── Update Training Method ────────────────────────────────────────────
|
|
@router.put("/methods/{method_id}")
|
|
def update_method(method_id: int, data: dict, session=Depends(require_auth)):
|
|
"""Update training method (admin only)."""
|
|
role = session.get('role')
|
|
if role not in ['admin', 'superadmin']:
|
|
raise HTTPException(403, "Nur Admins dürfen Trainingsmethoden bearbeiten")
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Check existence
|
|
cur.execute("SELECT id FROM training_methods WHERE id = %s", (method_id,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "Trainingsmethode nicht gefunden")
|
|
|
|
# Update
|
|
cur.execute("""
|
|
UPDATE training_methods SET
|
|
name = %s,
|
|
abbreviation = %s,
|
|
category = %s,
|
|
description = %s,
|
|
typical_duration = %s,
|
|
typical_group_size = %s,
|
|
related_skills = %s,
|
|
keywords = %s,
|
|
status = %s,
|
|
updated_at = NOW()
|
|
WHERE id = %s
|
|
""", (
|
|
data.get('name'),
|
|
data.get('abbreviation'),
|
|
data.get('category'),
|
|
data.get('description'),
|
|
data.get('typical_duration'),
|
|
data.get('typical_group_size'),
|
|
data.get('related_skills'),
|
|
data.get('keywords'),
|
|
data.get('status'),
|
|
method_id
|
|
))
|
|
|
|
conn.commit()
|
|
|
|
return get_method(method_id, session)
|
|
|
|
|
|
# ── Delete Training Method ────────────────────────────────────────────
|
|
@router.delete("/methods/{method_id}")
|
|
def delete_method(method_id: int, session=Depends(require_auth)):
|
|
"""Delete training method (superadmin only)."""
|
|
role = session.get('role')
|
|
if role != 'superadmin':
|
|
raise HTTPException(403, "Nur Superadmins dürfen Trainingsmethoden löschen")
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Check existence
|
|
cur.execute("SELECT id FROM training_methods WHERE id = %s", (method_id,))
|
|
if not cur.fetchone():
|
|
raise HTTPException(404, "Trainingsmethode nicht gefunden")
|
|
|
|
# Delete
|
|
cur.execute("DELETE FROM training_methods WHERE id = %s", (method_id,))
|
|
conn.commit()
|
|
|
|
return {"ok": True}
|