| """ |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| 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 |