Code Splitting Results: - goals.py: 1339 → 655 lines (-684 lines, -51%) - Created 4 new routers: * goal_types.py (426 lines) - Goal Type Definitions CRUD * goal_progress.py (155 lines) - Progress tracking * training_phases.py (107 lines) - Training phases * fitness_tests.py (94 lines) - Fitness tests Benefits: ✅ Improved maintainability (smaller, focused files) ✅ Better context window efficiency for AI tools ✅ Clearer separation of concerns ✅ Easier testing and debugging All routers registered in main.py. Backward compatible - no API changes.
156 lines
5.2 KiB
Python
156 lines
5.2 KiB
Python
"""
|
|
Goal Progress Router - Progress Tracking for Goals
|
|
|
|
Endpoints for logging and managing goal progress:
|
|
- Get progress history
|
|
- Create manual progress entries
|
|
- Delete progress entries
|
|
|
|
Part of v9h Goal System.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
from datetime import date
|
|
|
|
from db import get_db, get_cursor, r2d
|
|
from auth import require_auth
|
|
|
|
router = APIRouter(prefix="/api/goals", tags=["goal-progress"])
|
|
|
|
# ============================================================================
|
|
# Pydantic Models
|
|
# ============================================================================
|
|
|
|
class GoalProgressCreate(BaseModel):
|
|
"""Log progress for a goal"""
|
|
date: date
|
|
value: float
|
|
note: Optional[str] = None
|
|
|
|
class GoalProgressUpdate(BaseModel):
|
|
"""Update progress entry"""
|
|
value: Optional[float] = None
|
|
note: Optional[str] = None
|
|
|
|
# ============================================================================
|
|
# Endpoints
|
|
# ============================================================================
|
|
|
|
@router.get("/{goal_id}/progress")
|
|
def get_goal_progress(goal_id: str, session: dict = Depends(require_auth)):
|
|
"""Get progress history for a goal"""
|
|
pid = session['profile_id']
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Verify ownership
|
|
cur.execute(
|
|
"SELECT id FROM goals WHERE id = %s AND profile_id = %s",
|
|
(goal_id, pid)
|
|
)
|
|
if not cur.fetchone():
|
|
raise HTTPException(status_code=404, detail="Ziel nicht gefunden")
|
|
|
|
# Get progress entries
|
|
cur.execute("""
|
|
SELECT id, date, value, note, source, created_at
|
|
FROM goal_progress_log
|
|
WHERE goal_id = %s
|
|
ORDER BY date DESC
|
|
""", (goal_id,))
|
|
|
|
entries = cur.fetchall()
|
|
return [r2d(e) for e in entries]
|
|
|
|
@router.post("/{goal_id}/progress")
|
|
def create_goal_progress(goal_id: str, data: GoalProgressCreate, session: dict = Depends(require_auth)):
|
|
"""Log new progress for a goal"""
|
|
pid = session['profile_id']
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Verify ownership and check if manual entry is allowed
|
|
cur.execute("""
|
|
SELECT g.id, g.unit, gt.source_table
|
|
FROM goals g
|
|
LEFT JOIN goal_type_definitions gt ON g.goal_type = gt.type_key
|
|
WHERE g.id = %s AND g.profile_id = %s
|
|
""", (goal_id, pid))
|
|
goal = cur.fetchone()
|
|
if not goal:
|
|
raise HTTPException(status_code=404, detail="Ziel nicht gefunden")
|
|
|
|
# Prevent manual entries for goals with automatic data sources
|
|
if goal['source_table']:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Manuelle Einträge nicht erlaubt für automatisch erfasste Ziele. "
|
|
f"Bitte nutze die entsprechende Erfassungsseite (z.B. Gewicht, Aktivität)."
|
|
)
|
|
|
|
# Insert progress entry
|
|
try:
|
|
cur.execute("""
|
|
INSERT INTO goal_progress_log (goal_id, profile_id, date, value, note, source)
|
|
VALUES (%s, %s, %s, %s, %s, 'manual')
|
|
RETURNING id
|
|
""", (goal_id, pid, data.date, data.value, data.note))
|
|
|
|
progress_id = cur.fetchone()['id']
|
|
|
|
# Trigger will auto-update goals.current_value
|
|
return {
|
|
"id": progress_id,
|
|
"message": f"Fortschritt erfasst: {data.value} {goal['unit']}"
|
|
}
|
|
|
|
except Exception as e:
|
|
if "unique_progress_per_day" in str(e):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Für {data.date} existiert bereits ein Eintrag. Bitte bearbeite den existierenden Eintrag."
|
|
)
|
|
raise HTTPException(status_code=500, detail=f"Fehler beim Speichern: {str(e)}")
|
|
|
|
@router.delete("/{goal_id}/progress/{progress_id}")
|
|
def delete_goal_progress(goal_id: str, progress_id: str, session: dict = Depends(require_auth)):
|
|
"""Delete progress entry"""
|
|
pid = session['profile_id']
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
|
|
# Verify ownership
|
|
cur.execute(
|
|
"SELECT id FROM goals WHERE id = %s AND profile_id = %s",
|
|
(goal_id, pid)
|
|
)
|
|
if not cur.fetchone():
|
|
raise HTTPException(status_code=404, detail="Ziel nicht gefunden")
|
|
|
|
# Delete progress entry
|
|
cur.execute(
|
|
"DELETE FROM goal_progress_log WHERE id = %s AND goal_id = %s AND profile_id = %s",
|
|
(progress_id, goal_id, pid)
|
|
)
|
|
|
|
if cur.rowcount == 0:
|
|
raise HTTPException(status_code=404, detail="Progress-Eintrag nicht gefunden")
|
|
|
|
# After deletion, recalculate current_value from remaining entries
|
|
cur.execute("""
|
|
UPDATE goals
|
|
SET current_value = (
|
|
SELECT value FROM goal_progress_log
|
|
WHERE goal_id = %s
|
|
ORDER BY date DESC
|
|
LIMIT 1
|
|
)
|
|
WHERE id = %s
|
|
""", (goal_id, goal_id))
|
|
|
|
return {"message": "Progress-Eintrag gelöscht"}
|