feat: goal categories and priorities - backend + API
Implemented multi-dimensional goal priorities (Option B). **Backend Changes:** - Migration 028: Added `category` + `priority` columns to goals table - Auto-migration of existing goals to categories based on goal_type - GoalCreate/GoalUpdate models extended with category + priority - New endpoint: GET /api/goals/grouped (returns goals by category) - Categories: body, training, nutrition, recovery, health, other - Priorities: 1=high (⭐⭐⭐), 2=medium (⭐⭐), 3=low (⭐) **API Changes:** - Added api.listGoalsGrouped() binding **Frontend (partial):** - Added GOAL_CATEGORIES + PRIORITY_LEVELS constants - Extended formData with category + priority fields - Removed "Gewichtung gesamt" display (useless) - Load groupedGoals in addition to flat goals list Next: Complete frontend UI rebuild for category grouping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2f51b26418
commit
6a3a782bff
58
backend/migrations/028_goal_categories_priorities.sql
Normal file
58
backend/migrations/028_goal_categories_priorities.sql
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
-- Migration 028: Goal Categories and Priorities
|
||||
-- Date: 2026-03-27
|
||||
-- Purpose: Multi-dimensional goal priorities (one primary goal per category)
|
||||
|
||||
-- ============================================================================
|
||||
-- Add category and priority columns
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE goals
|
||||
ADD COLUMN category VARCHAR(50),
|
||||
ADD COLUMN priority INTEGER DEFAULT 2 CHECK (priority >= 1 AND priority <= 3);
|
||||
|
||||
COMMENT ON COLUMN goals.category IS 'Goal category: body, training, nutrition, recovery, health, other';
|
||||
COMMENT ON COLUMN goals.priority IS 'Priority level: 1=high, 2=medium, 3=low';
|
||||
|
||||
-- ============================================================================
|
||||
-- Migrate existing goals to categories based on goal_type
|
||||
-- ============================================================================
|
||||
|
||||
UPDATE goals SET category = CASE
|
||||
-- Body composition goals
|
||||
WHEN goal_type IN ('weight', 'body_fat', 'lean_mass') THEN 'body'
|
||||
|
||||
-- Training goals
|
||||
WHEN goal_type IN ('strength', 'flexibility', 'training_frequency') THEN 'training'
|
||||
|
||||
-- Health/cardio goals
|
||||
WHEN goal_type IN ('vo2max', 'rhr', 'bp', 'hrv') THEN 'health'
|
||||
|
||||
-- Recovery goals
|
||||
WHEN goal_type IN ('sleep_quality', 'sleep_duration', 'rest_days') THEN 'recovery'
|
||||
|
||||
-- Nutrition goals
|
||||
WHEN goal_type IN ('calories', 'protein', 'healthy_eating') THEN 'nutrition'
|
||||
|
||||
-- Default
|
||||
ELSE 'other'
|
||||
END
|
||||
WHERE category IS NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- Set priority based on is_primary
|
||||
-- ============================================================================
|
||||
|
||||
UPDATE goals SET priority = CASE
|
||||
WHEN is_primary = true THEN 1 -- Primary goals get priority 1
|
||||
ELSE 2 -- Others get priority 2 (medium)
|
||||
END;
|
||||
|
||||
-- ============================================================================
|
||||
-- Create index for category-based queries
|
||||
-- ============================================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_goals_category_priority
|
||||
ON goals(profile_id, category, priority)
|
||||
WHERE is_active = true;
|
||||
|
||||
COMMENT ON INDEX idx_goals_category_priority IS 'Fast lookup for category-grouped goals sorted by priority';
|
||||
|
|
@ -42,10 +42,12 @@ class FocusAreasUpdate(BaseModel):
|
|||
class GoalCreate(BaseModel):
|
||||
"""Create or update a concrete goal"""
|
||||
goal_type: str # weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr
|
||||
is_primary: bool = False
|
||||
is_primary: bool = False # Kept for backward compatibility
|
||||
target_value: float
|
||||
unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps
|
||||
target_date: Optional[date] = None
|
||||
category: Optional[str] = 'other' # body, training, nutrition, recovery, health, other
|
||||
priority: Optional[int] = 2 # 1=high, 2=medium, 3=low
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
|
@ -54,7 +56,9 @@ class GoalUpdate(BaseModel):
|
|||
target_value: Optional[float] = None
|
||||
target_date: Optional[date] = None
|
||||
status: Optional[str] = None # active, reached, abandoned, expired
|
||||
is_primary: Optional[bool] = None
|
||||
is_primary: Optional[bool] = None # Kept for backward compatibility
|
||||
category: Optional[str] = None # body, training, nutrition, recovery, health, other
|
||||
priority: Optional[int] = None # 1=high, 2=medium, 3=low
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
|
@ -403,13 +407,13 @@ def create_goal(data: GoalCreate, session: dict = Depends(require_auth)):
|
|||
INSERT INTO goals (
|
||||
profile_id, goal_type, is_primary,
|
||||
target_value, current_value, start_value, unit,
|
||||
target_date, name, description
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
target_date, category, priority, name, description
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (
|
||||
pid, data.goal_type, data.is_primary,
|
||||
data.target_value, current_value, current_value, data.unit,
|
||||
data.target_date, data.name, data.description
|
||||
data.target_date, data.category, data.priority, data.name, data.description
|
||||
))
|
||||
|
||||
goal_id = cur.fetchone()['id']
|
||||
|
|
@ -461,6 +465,14 @@ def update_goal(goal_id: str, data: GoalUpdate, session: dict = Depends(require_
|
|||
updates.append("is_primary = %s")
|
||||
params.append(data.is_primary)
|
||||
|
||||
if data.category is not None:
|
||||
updates.append("category = %s")
|
||||
params.append(data.category)
|
||||
|
||||
if data.priority is not None:
|
||||
updates.append("priority = %s")
|
||||
params.append(data.priority)
|
||||
|
||||
if data.name is not None:
|
||||
updates.append("name = %s")
|
||||
params.append(data.name)
|
||||
|
|
@ -499,6 +511,52 @@ def delete_goal(goal_id: str, session: dict = Depends(require_auth)):
|
|||
|
||||
return {"message": "Ziel gelöscht"}
|
||||
|
||||
@router.get("/grouped")
|
||||
def get_goals_grouped(session: dict = Depends(require_auth)):
|
||||
"""
|
||||
Get goals grouped by category, sorted by priority.
|
||||
|
||||
Returns structure:
|
||||
{
|
||||
"body": [{"id": "...", "goal_type": "weight", "priority": 1, ...}, ...],
|
||||
"training": [...],
|
||||
"nutrition": [...],
|
||||
"recovery": [...],
|
||||
"health": [...],
|
||||
"other": [...]
|
||||
}
|
||||
"""
|
||||
pid = session['profile_id']
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Get all active goals with type definitions
|
||||
cur.execute("""
|
||||
SELECT
|
||||
g.id, g.goal_type, g.target_value, g.current_value, g.start_value,
|
||||
g.unit, g.target_date, g.status, g.is_primary, g.category, g.priority,
|
||||
g.name, g.description, g.progress_pct, g.linear_projection,
|
||||
g.created_at, g.updated_at,
|
||||
gt.label_de, gt.icon, gt.category as type_category
|
||||
FROM goals g
|
||||
LEFT JOIN goal_type_definitions gt ON g.goal_type = gt.type_key
|
||||
WHERE g.profile_id = %s AND g.is_active = true
|
||||
ORDER BY g.category, g.priority ASC, g.created_at DESC
|
||||
""", (pid,))
|
||||
|
||||
goals = cur.fetchall()
|
||||
|
||||
# Group by category
|
||||
grouped = {}
|
||||
for goal in goals:
|
||||
cat = goal['category'] or 'other'
|
||||
if cat not in grouped:
|
||||
grouped[cat] = []
|
||||
grouped[cat].append(r2d(goal))
|
||||
|
||||
return grouped
|
||||
|
||||
# ============================================================================
|
||||
# Training Phases
|
||||
# ============================================================================
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ export const api = {
|
|||
updateFocusAreas: (d) => req('/goals/focus-areas', jput(d)),
|
||||
|
||||
listGoals: () => req('/goals/list'),
|
||||
listGoalsGrouped: () => req('/goals/grouped'),
|
||||
createGoal: (d) => req('/goals/create', json(d)),
|
||||
updateGoal: (id,d) => req(`/goals/${id}`, jput(d)),
|
||||
deleteGoal: (id) => req(`/goals/${id}`, {method:'DELETE'}),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user