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
|
target_value: float
|
||||||
unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps
|
unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps
|
||||||
target_date: Optional[date] = None
|
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
|
category: Optional[str] = 'other' # body, training, nutrition, recovery, health, other
|
||||||
priority: Optional[int] = 2 # 1=high, 2=medium, 3=low
|
priority: Optional[int] = 2 # 1=high, 2=medium, 3=low
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
@ -64,6 +66,8 @@ class GoalUpdate(BaseModel):
|
||||||
"""Update existing goal"""
|
"""Update existing goal"""
|
||||||
target_value: Optional[float] = None
|
target_value: Optional[float] = None
|
||||||
target_date: Optional[date] = 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
|
status: Optional[str] = None # active, reached, abandoned, expired
|
||||||
is_primary: Optional[bool] = None # Kept for backward compatibility
|
is_primary: Optional[bool] = None # Kept for backward compatibility
|
||||||
category: Optional[str] = None # body, training, nutrition, recovery, health, other
|
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
|
# Get current value for this goal type
|
||||||
current_value = _get_current_value_for_goal_type(conn, pid, data.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
|
# Insert goal
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO goals (
|
INSERT INTO goals (
|
||||||
profile_id, goal_type, is_primary,
|
profile_id, goal_type, is_primary,
|
||||||
target_value, current_value, start_value, unit,
|
target_value, current_value, start_value, unit,
|
||||||
target_date, category, priority, name, description
|
start_date, target_date, category, priority, name, description
|
||||||
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (
|
""", (
|
||||||
pid, data.goal_type, data.is_primary,
|
pid, data.goal_type, data.is_primary,
|
||||||
data.target_value, current_value, current_value, data.unit,
|
data.target_value, current_value, start_value, data.unit,
|
||||||
data.target_date, data.category, data.priority, data.name, data.description
|
start_date, data.target_date, data.category, data.priority, data.name, data.description
|
||||||
))
|
))
|
||||||
|
|
||||||
goal_id = cur.fetchone()['id']
|
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")
|
updates.append("description = %s")
|
||||||
params.append(data.description)
|
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)
|
# Handle focus_contributions separately (can be updated even if no other changes)
|
||||||
if data.focus_contributions is not None:
|
if data.focus_contributions is not None:
|
||||||
# Delete existing contributions
|
# 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)
|
# Delegate to universal fetcher (Phase 1.5)
|
||||||
return get_current_value_for_goal(conn, profile_id, goal_type)
|
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):
|
def _update_goal_progress(conn, profile_id: str, goal: dict):
|
||||||
"""Update goal progress (modifies goal dict in-place)"""
|
"""Update goal progress (modifies goal dict in-place)"""
|
||||||
# Get current value
|
# Get current value
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user