Spaces:
Sleeping
Sleeping
| """ | |
| utils/progress_tracker.py β Daily progress tracking logic for FitPlan Pro. | |
| Handles: checkbox state management, completion detection, stats calculation. | |
| """ | |
| from utils.db import save_progress, get_progress, get_all_progress | |
| from utils.streak_manager import update_streak | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Checkbox state helpers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_workout_checks(exercises): | |
| """ | |
| Build initial empty checkbox dict from exercise list. | |
| exercises: list of dicts with 'name' key | |
| Returns: {"Push-ups": False, "Squats": False, ...} | |
| """ | |
| return {ex["name"]: False for ex in exercises if "name" in ex} | |
| def build_dietary_checks(dietary): | |
| """ | |
| Build initial empty checkbox dict from dietary dict. | |
| dietary: {"breakfast": "...", "lunch": "...", ...} | |
| Returns: {"breakfast": False, "lunch": False, ...} | |
| """ | |
| return {meal: False for meal in dietary if dietary[meal]} | |
| def merge_checks(existing, new_keys): | |
| """ | |
| Merge saved checkbox state with current exercise/meal list. | |
| Preserves saved True/False values; adds False for new keys. | |
| """ | |
| merged = {} | |
| for key in new_keys: | |
| merged[key] = existing.get(key, False) | |
| return merged | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Core tracking functions | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_day_progress(username, plan_id, day_number, exercises, dietary): | |
| """ | |
| Load saved progress for a day and merge with current plan data. | |
| Returns: (workout_checks, dietary_checks, day_completed) | |
| """ | |
| saved = get_progress(username, plan_id, day_number) | |
| workout_keys = [ex["name"] for ex in exercises if "name" in ex] | |
| dietary_keys = list(dietary.keys()) if dietary else [] | |
| workout_checks = merge_checks(saved["workout_checks"], workout_keys) | |
| dietary_checks = merge_checks(saved["dietary_checks"], dietary_keys) | |
| return workout_checks, dietary_checks, saved["day_completed"] | |
| def toggle_workout_item(username, plan_id, day_number, | |
| exercise_name, checked, | |
| current_workout_checks, current_dietary_checks): | |
| """ | |
| Toggle a single workout checkbox and save to DB. | |
| Returns: (updated_workout_checks, day_completed, new_streak) | |
| """ | |
| current_workout_checks[exercise_name] = checked | |
| day_completed = save_progress( | |
| username, plan_id, day_number, | |
| current_workout_checks, current_dietary_checks | |
| ) | |
| new_streak = update_streak(username, day_completed) | |
| return current_workout_checks, day_completed, new_streak | |
| def toggle_dietary_item(username, plan_id, day_number, | |
| meal_name, checked, | |
| current_workout_checks, current_dietary_checks): | |
| """ | |
| Toggle a single dietary checkbox and save to DB. | |
| Returns: (updated_dietary_checks, day_completed, new_streak) | |
| """ | |
| current_dietary_checks[meal_name] = checked | |
| day_completed = save_progress( | |
| username, plan_id, day_number, | |
| current_workout_checks, current_dietary_checks | |
| ) | |
| new_streak = update_streak(username, day_completed) | |
| return current_dietary_checks, day_completed, new_streak | |
| def mark_day_complete(username, plan_id, day_number, | |
| exercises, dietary): | |
| """ | |
| Mark ALL items in a day as complete at once. | |
| Returns: (workout_checks, dietary_checks, new_streak) | |
| """ | |
| workout_checks = {ex["name"]: True for ex in exercises if "name" in ex} | |
| dietary_checks = {meal: True for meal in dietary if dietary[meal]} | |
| save_progress(username, plan_id, day_number, workout_checks, dietary_checks) | |
| new_streak = update_streak(username, True) | |
| return workout_checks, dietary_checks, new_streak | |
| def unmark_day(username, plan_id, day_number, exercises, dietary): | |
| """ | |
| Unmark ALL items in a day (reset to unchecked). | |
| Returns: (workout_checks, dietary_checks) | |
| """ | |
| workout_checks = {ex["name"]: False for ex in exercises if "name" in ex} | |
| dietary_checks = {meal: False for meal in dietary if dietary[meal]} | |
| save_progress(username, plan_id, day_number, workout_checks, dietary_checks) | |
| update_streak(username, False) | |
| return workout_checks, dietary_checks | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Stats / Summary | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_plan_stats(username, plan_id, total_days): | |
| """ | |
| Calculate overall plan completion stats. | |
| Returns dict with summary numbers. | |
| """ | |
| all_progress = get_all_progress(username, plan_id) | |
| completed_days = sum(1 for p in all_progress if p["day_completed"]) | |
| total_workout_tasks = 0 | |
| done_workout_tasks = 0 | |
| total_dietary_tasks = 0 | |
| done_dietary_tasks = 0 | |
| for p in all_progress: | |
| wc = p["workout_checks"] | |
| dc = p["dietary_checks"] | |
| total_workout_tasks += len(wc) | |
| done_workout_tasks += sum(1 for v in wc.values() if v) | |
| total_dietary_tasks += len(dc) | |
| done_dietary_tasks += sum(1 for v in dc.values() if v) | |
| total_tasks = total_workout_tasks + total_dietary_tasks | |
| done_tasks = done_workout_tasks + done_dietary_tasks | |
| return { | |
| "completed_days": completed_days, | |
| "total_days": total_days, | |
| "pending_days": total_days - completed_days, | |
| "progress_pct": round((completed_days / total_days * 100), 1) if total_days else 0, | |
| "total_workout_tasks": total_workout_tasks, | |
| "done_workout_tasks": done_workout_tasks, | |
| "total_dietary_tasks": total_dietary_tasks, | |
| "done_dietary_tasks": done_dietary_tasks, | |
| "total_tasks": total_tasks, | |
| "done_tasks": done_tasks, | |
| "overall_task_pct": round((done_tasks / total_tasks * 100), 1) if total_tasks else 0 | |
| } | |
| def get_today_day_number(plan_created_at, days_per_week=5): | |
| """ | |
| Calculate which day of the plan today corresponds to. | |
| Based on days since plan creation and training frequency. | |
| Returns: day_number (1-indexed), or 1 if before start. | |
| """ | |
| from datetime import datetime, timezone | |
| import math | |
| now = datetime.now(timezone.utc).timestamp() | |
| days_elapsed = (now - plan_created_at) / 86400 # seconds per day | |
| # Account for rest days: only training days count | |
| # e.g. 5 days/week = train Mon-Fri, rest Sat-Sun | |
| training_day = math.floor(days_elapsed * (days_per_week / 7)) + 1 | |
| return max(1, int(training_day)) | |
| def get_completion_heatmap(username, plan_id, total_days): | |
| """ | |
| Build a completion status list for calendar/heatmap display. | |
| Returns: list of dicts [{day_number, status, date}] | |
| status: 'completed' | 'partial' | 'pending' | |
| """ | |
| all_progress = get_all_progress(username, plan_id) | |
| progress_map = {p["day_number"]: p for p in all_progress} | |
| heatmap = [] | |
| for day in range(1, total_days + 1): | |
| p = progress_map.get(day) | |
| if not p: | |
| status = "pending" | |
| elif p["day_completed"]: | |
| status = "completed" | |
| else: | |
| wc = p["workout_checks"] | |
| dc = p["dietary_checks"] | |
| any_done = any(wc.values()) or any(dc.values()) | |
| status = "partial" if any_done else "pending" | |
| heatmap.append({ | |
| "day_number": day, | |
| "status": status, | |
| "date": p["date"] if p else None | |
| }) | |
| return heatmap |