mitai-jinkendo/backend/routers/goal_progress.py
Lars 12d516c881
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
refactor: split goals.py into 5 modular routers
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.
2026-03-28 06:31:31 +01:00

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"}