fix: behind_schedule now uses time-based deviation, not just lowest progress
OLD: Showed 3 goals with lowest progress %
NEW: Calculates expected progress based on elapsed time vs. total time
Shows goals with largest negative deviation (behind schedule)
Example Weight Goal:
- Total time: 98 days (22.02 - 31.05)
- Elapsed: 34 days (35%)
- Actual progress: 41%
- Deviation: +7% (AHEAD, not behind)
Also updated on_track to show goals with positive deviation (ahead of schedule).
Note: Linear progress is a simplification. Real-world progress curves vary
by goal type (weight loss, muscle gain, VO2max, etc). Future: AI-based
projection models for more realistic expectations.
This commit is contained in:
parent
d7aa0eb3af
commit
8e67175ed2
|
|
@ -806,111 +806,218 @@ def _format_top_focus_areas(profile_id: str, n: int = 3) -> str:
|
|||
|
||||
|
||||
def _format_goals_behind(profile_id: str, n: int = 3) -> str:
|
||||
"""Format top N goals behind schedule"""
|
||||
"""
|
||||
Format top N goals behind schedule (based on time deviation).
|
||||
|
||||
Compares actual progress vs. expected progress based on elapsed time.
|
||||
Negative deviation = behind schedule.
|
||||
"""
|
||||
try:
|
||||
from goal_utils import get_active_goals
|
||||
from datetime import date
|
||||
|
||||
goals = get_active_goals(profile_id)
|
||||
|
||||
if not goals:
|
||||
return 'Keine Ziele definiert'
|
||||
|
||||
# Calculate progress for each goal
|
||||
goals_with_progress = []
|
||||
today = date.today()
|
||||
goals_with_deviation = []
|
||||
|
||||
for g in goals:
|
||||
current = g.get('current_value')
|
||||
target = g.get('target_value')
|
||||
start = g.get('start_value')
|
||||
start_date = g.get('start_date')
|
||||
target_date = g.get('target_date')
|
||||
|
||||
# Skip if missing required values
|
||||
if None in [current, target, start]:
|
||||
continue
|
||||
|
||||
# Calculate progress percentage
|
||||
# Skip if no target_date (can't calculate time-based progress)
|
||||
if not target_date:
|
||||
continue
|
||||
|
||||
try:
|
||||
current = float(current)
|
||||
target = float(target)
|
||||
start = float(start)
|
||||
|
||||
# Calculate actual progress percentage
|
||||
if target == start:
|
||||
progress_pct = 100 if current == target else 0
|
||||
actual_progress_pct = 100 if current == target else 0
|
||||
else:
|
||||
progress_pct = ((current - start) / (target - start)) * 100
|
||||
progress_pct = max(0, min(100, progress_pct))
|
||||
actual_progress_pct = ((current - start) / (target - start)) * 100
|
||||
actual_progress_pct = max(0, min(100, actual_progress_pct))
|
||||
|
||||
g['_calc_progress_pct'] = int(progress_pct)
|
||||
goals_with_progress.append(g)
|
||||
except (ValueError, ZeroDivisionError):
|
||||
# Calculate expected progress based on time
|
||||
if start_date:
|
||||
# Use start_date if available
|
||||
start_dt = start_date if isinstance(start_date, date) else date.fromisoformat(str(start_date))
|
||||
else:
|
||||
# Fallback: assume start date = created_at date
|
||||
created_at = g.get('created_at')
|
||||
if created_at:
|
||||
start_dt = date.fromisoformat(str(created_at).split('T')[0])
|
||||
else:
|
||||
continue # Can't calculate without start date
|
||||
|
||||
target_dt = target_date if isinstance(target_date, date) else date.fromisoformat(str(target_date))
|
||||
|
||||
# Calculate time progress
|
||||
total_days = (target_dt - start_dt).days
|
||||
elapsed_days = (today - start_dt).days
|
||||
|
||||
if total_days <= 0:
|
||||
continue # Invalid date range
|
||||
|
||||
expected_progress_pct = (elapsed_days / total_days) * 100
|
||||
expected_progress_pct = max(0, min(100, expected_progress_pct))
|
||||
|
||||
# Calculate deviation (negative = behind schedule)
|
||||
deviation = actual_progress_pct - expected_progress_pct
|
||||
|
||||
g['_actual_progress'] = int(actual_progress_pct)
|
||||
g['_expected_progress'] = int(expected_progress_pct)
|
||||
g['_deviation'] = int(deviation)
|
||||
goals_with_deviation.append(g)
|
||||
|
||||
except (ValueError, ZeroDivisionError, TypeError):
|
||||
continue
|
||||
|
||||
if not goals_with_progress:
|
||||
return 'Keine Ziele mit Fortschritt'
|
||||
if not goals_with_deviation:
|
||||
return 'Keine Ziele mit Zeitvorgabe'
|
||||
|
||||
# Sort by progress ascending (lowest first) and take top N
|
||||
sorted_goals = sorted(goals_with_progress, key=lambda x: x.get('_calc_progress_pct', 0))[:n]
|
||||
# Sort by deviation ascending (most negative first = most behind)
|
||||
# Only include goals that are actually behind (deviation < 0)
|
||||
behind_goals = [g for g in goals_with_deviation if g['_deviation'] < 0]
|
||||
|
||||
if not behind_goals:
|
||||
return 'Alle Ziele im Zeitplan'
|
||||
|
||||
sorted_goals = sorted(behind_goals, key=lambda x: x['_deviation'])[:n]
|
||||
|
||||
lines = []
|
||||
for goal in sorted_goals:
|
||||
name = goal.get('name') or goal.get('goal_type', 'Unbekannt')
|
||||
progress = goal.get('_calc_progress_pct', 0)
|
||||
lines.append(f"{name} ({progress}%)")
|
||||
actual = goal['_actual_progress']
|
||||
expected = goal['_expected_progress']
|
||||
deviation = goal['_deviation']
|
||||
lines.append(f"{name} ({actual}% statt {expected}%, {deviation}%)")
|
||||
|
||||
return ', '.join(lines)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(f"[ERROR] _format_goals_behind: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
|
||||
|
||||
def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
||||
"""Format top N goals on track"""
|
||||
"""
|
||||
Format top N goals ahead of schedule (based on time deviation).
|
||||
|
||||
Compares actual progress vs. expected progress based on elapsed time.
|
||||
Positive deviation = ahead of schedule / on track.
|
||||
"""
|
||||
try:
|
||||
from goal_utils import get_active_goals
|
||||
from datetime import date
|
||||
|
||||
goals = get_active_goals(profile_id)
|
||||
|
||||
if not goals:
|
||||
return 'Keine Ziele definiert'
|
||||
|
||||
# Calculate progress for each goal
|
||||
goals_with_progress = []
|
||||
today = date.today()
|
||||
goals_with_deviation = []
|
||||
|
||||
for g in goals:
|
||||
current = g.get('current_value')
|
||||
target = g.get('target_value')
|
||||
start = g.get('start_value')
|
||||
start_date = g.get('start_date')
|
||||
target_date = g.get('target_date')
|
||||
|
||||
# Skip if missing required values
|
||||
if None in [current, target, start]:
|
||||
continue
|
||||
|
||||
# Calculate progress percentage
|
||||
# Skip if no target_date
|
||||
if not target_date:
|
||||
continue
|
||||
|
||||
try:
|
||||
current = float(current)
|
||||
target = float(target)
|
||||
start = float(start)
|
||||
|
||||
# Calculate actual progress percentage
|
||||
if target == start:
|
||||
progress_pct = 100 if current == target else 0
|
||||
actual_progress_pct = 100 if current == target else 0
|
||||
else:
|
||||
progress_pct = ((current - start) / (target - start)) * 100
|
||||
progress_pct = max(0, min(100, progress_pct))
|
||||
actual_progress_pct = ((current - start) / (target - start)) * 100
|
||||
actual_progress_pct = max(0, min(100, actual_progress_pct))
|
||||
|
||||
g['_calc_progress_pct'] = int(progress_pct)
|
||||
goals_with_progress.append(g)
|
||||
except (ValueError, ZeroDivisionError):
|
||||
# Calculate expected progress based on time
|
||||
if start_date:
|
||||
start_dt = start_date if isinstance(start_date, date) else date.fromisoformat(str(start_date))
|
||||
else:
|
||||
created_at = g.get('created_at')
|
||||
if created_at:
|
||||
start_dt = date.fromisoformat(str(created_at).split('T')[0])
|
||||
else:
|
||||
continue
|
||||
|
||||
# Filter goals with progress >= 50%
|
||||
goals_on_track = [g for g in goals_with_progress if g.get('_calc_progress_pct', 0) >= 50]
|
||||
target_dt = target_date if isinstance(target_date, date) else date.fromisoformat(str(target_date))
|
||||
|
||||
if not goals_on_track:
|
||||
return 'Keine Ziele auf gutem Weg'
|
||||
# Calculate time progress
|
||||
total_days = (target_dt - start_dt).days
|
||||
elapsed_days = (today - start_dt).days
|
||||
|
||||
# Sort by progress descending (highest first) and take top N
|
||||
sorted_goals = sorted(goals_on_track, key=lambda x: x.get('_calc_progress_pct', 0), reverse=True)[:n]
|
||||
if total_days <= 0:
|
||||
continue
|
||||
|
||||
expected_progress_pct = (elapsed_days / total_days) * 100
|
||||
expected_progress_pct = max(0, min(100, expected_progress_pct))
|
||||
|
||||
# Calculate deviation (positive = ahead of schedule)
|
||||
deviation = actual_progress_pct - expected_progress_pct
|
||||
|
||||
g['_actual_progress'] = int(actual_progress_pct)
|
||||
g['_expected_progress'] = int(expected_progress_pct)
|
||||
g['_deviation'] = int(deviation)
|
||||
goals_with_deviation.append(g)
|
||||
|
||||
except (ValueError, ZeroDivisionError, TypeError):
|
||||
continue
|
||||
|
||||
if not goals_with_deviation:
|
||||
return 'Keine Ziele mit Zeitvorgabe'
|
||||
|
||||
# Sort by deviation descending (most positive first = most ahead)
|
||||
# Only include goals that are ahead or on track (deviation >= 0)
|
||||
ahead_goals = [g for g in goals_with_deviation if g['_deviation'] >= 0]
|
||||
|
||||
if not ahead_goals:
|
||||
return 'Keine Ziele im Zeitplan'
|
||||
|
||||
sorted_goals = sorted(ahead_goals, key=lambda x: x['_deviation'], reverse=True)[:n]
|
||||
|
||||
lines = []
|
||||
for goal in sorted_goals:
|
||||
name = goal.get('name') or goal.get('goal_type', 'Unbekannt')
|
||||
progress = goal.get('_calc_progress_pct', 0)
|
||||
lines.append(f"{name} ({progress}%)")
|
||||
actual = goal['_actual_progress']
|
||||
expected = goal['_expected_progress']
|
||||
deviation = goal['_deviation']
|
||||
lines.append(f"{name} ({actual}%, +{deviation}% voraus)")
|
||||
|
||||
return ', '.join(lines)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
print(f"[ERROR] _format_goals_on_track: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user