diff --git a/backend/routers/goals.py b/backend/routers/goals.py index 7162f6d..8ab0877 100644 --- a/backend/routers/goals.py +++ b/backend/routers/goals.py @@ -54,6 +54,8 @@ class GoalCreate(BaseModel): target_value: float unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps target_date: Optional[date] = None + start_date: Optional[date] = None # When goal started (defaults to today, can be historical) + start_value: Optional[float] = None # Auto-populated from start_date if not provided category: Optional[str] = 'other' # body, training, nutrition, recovery, health, other priority: Optional[int] = 2 # 1=high, 2=medium, 3=low name: Optional[str] = None @@ -64,6 +66,8 @@ class GoalUpdate(BaseModel): """Update existing goal""" target_value: Optional[float] = None target_date: Optional[date] = None + start_date: Optional[date] = None # Change start date (recalculates start_value) + start_value: Optional[float] = None # Manually override start value status: Optional[str] = None # active, reached, abandoned, expired is_primary: Optional[bool] = None # Kept for backward compatibility category: Optional[str] = None # body, training, nutrition, recovery, health, other @@ -382,18 +386,36 @@ def create_goal(data: GoalCreate, session: dict = Depends(require_auth)): # Get current value for this goal type current_value = _get_current_value_for_goal_type(conn, pid, data.goal_type) + # Determine start_date (default to today if not provided) + start_date = data.start_date if data.start_date else date.today() + + # Determine start_value + if data.start_value is not None: + # User explicitly provided start_value + start_value = data.start_value + elif start_date < date.today(): + # Historical start date - try to get historical value + start_value = _get_historical_value_for_goal_type(conn, pid, data.goal_type, start_date) + if start_value is None: + # No data on that date, fall back to current value + start_value = current_value + print(f"[WARN] No historical data for {data.goal_type} on {start_date}, using current value") + else: + # Start date is today, use current value + start_value = current_value + # Insert goal cur.execute(""" INSERT INTO goals ( profile_id, goal_type, is_primary, target_value, current_value, start_value, unit, - target_date, category, priority, name, description - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + start_date, target_date, category, priority, name, description + ) VALUES (%s, %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.category, data.priority, data.name, data.description + data.target_value, current_value, start_value, data.unit, + start_date, data.target_date, data.category, data.priority, data.name, data.description )) goal_id = cur.fetchone()['id'] @@ -472,6 +494,28 @@ def update_goal(goal_id: str, data: GoalUpdate, session: dict = Depends(require_ updates.append("description = %s") params.append(data.description) + # Handle start_date and start_value + if data.start_date is not None: + updates.append("start_date = %s") + params.append(data.start_date) + + # If start_value not explicitly provided, recalculate from historical data + if data.start_value is None: + # Get goal_type for historical lookup + cur.execute("SELECT goal_type FROM goals WHERE id = %s", (goal_id,)) + goal_row = cur.fetchone() + if goal_row: + goal_type = goal_row['goal_type'] + historical_value = _get_historical_value_for_goal_type(conn, pid, goal_type, data.start_date) + if historical_value is not None: + updates.append("start_value = %s") + params.append(historical_value) + print(f"[INFO] Auto-populated start_value from {data.start_date}: {historical_value}") + + if data.start_value is not None: + updates.append("start_value = %s") + params.append(data.start_value) + # Handle focus_contributions separately (can be updated even if no other changes) if data.focus_contributions is not None: # Delete existing contributions @@ -625,6 +669,70 @@ def _get_current_value_for_goal_type(conn, profile_id: str, goal_type: str) -> O # Delegate to universal fetcher (Phase 1.5) return get_current_value_for_goal(conn, profile_id, goal_type) +def _get_historical_value_for_goal_type(conn, profile_id: str, goal_type: str, target_date: date) -> Optional[float]: + """ + Get historical value for a goal type on a specific date. + Looks for closest value within ±7 days window. + + Args: + conn: Database connection + profile_id: User's profile ID + goal_type: Goal type key (e.g., 'weight', 'body_fat') + target_date: Date to query (can be historical) + + Returns: + Historical value or None if not found + """ + from goal_utils import get_goal_type_config, get_cursor + + # Get goal type configuration + config = get_goal_type_config(conn, goal_type) + if not config: + return None + + source_table = config.get('source_table') + source_column = config.get('source_column') + + if not source_table or not source_column: + return None + + # Query for value closest to target_date (±7 days window) + cur = get_cursor(conn) + + try: + # Special handling for different tables + if source_table == 'vitals_baseline': + date_col = 'date' + elif source_table == 'blood_pressure_log': + date_col = 'recorded_at::date' + else: + date_col = 'date' + + cur.execute(f""" + SELECT {source_column} + FROM {source_table} + WHERE profile_id = %s + AND {date_col} BETWEEN %s AND %s + ORDER BY ABS(EXTRACT(EPOCH FROM ({date_col} - %s::date))) + LIMIT 1 + """, ( + profile_id, + target_date - timedelta(days=7), + target_date + timedelta(days=7), + target_date + )) + + row = cur.fetchone() + if row: + value = row[source_column] + # Convert Decimal to float + return float(value) if value is not None else None + + return None + except Exception as e: + print(f"[ERROR] Failed to get historical value for {goal_type} on {target_date}: {e}") + return None + def _update_goal_progress(conn, profile_id: str, goal: dict): """Update goal progress (modifies goal dict in-place)""" # Get current value