feat: Auto-populate goal start_value from historical data
**Problem:** Goals created today had start_value = current_value, showing 0% progress even after months of tracking. **Solution:** 1. Added start_date and start_value to GoalCreate/GoalUpdate models 2. New function _get_historical_value_for_goal_type(): - Queries source table for value on specific date - ±7 day window for closest match - Works with all goal types via goal_type_definitions 3. create_goal() logic: - If start_date < today → auto-populate from historical data - If start_date = today → use current value - User can override start_value manually 4. update_goal() logic: - Changing start_date recalculates start_value - Can manually override start_value **Example:** - Goal created today with start_date = 3 months ago - System finds weight on that date (88 kg) - Current weight: 85.2 kg, Target: 82 kg - Progress: (85.2 - 88) / (82 - 88) = 47% ✓ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a6701bf7b2
commit
efde158dd4
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user