From 8e67175ed21e8f7b0cca28e951a7aaf5be021da1 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 28 Mar 2026 14:58:50 +0100 Subject: [PATCH] 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. --- backend/placeholder_resolver.py | 179 +++++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 36 deletions(-) diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index 5e23ffa..ba628dd 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -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 + + 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 + + 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 - # Filter goals with progress >= 50% - goals_on_track = [g for g in goals_with_progress if g.get('_calc_progress_pct', 0) >= 50] + if not goals_with_deviation: + return 'Keine Ziele mit Zeitvorgabe' - if not goals_on_track: - return 'Keine Ziele auf gutem Weg' + # 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] - # 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 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'