Fix: per-date workout completion in calendar (KC-10)
The calendar marked every occurrence of a program complete once any one was finished — including future dates — because DayTimelineView read the program's live shared doneSets. - Add WorkoutViewModel.isWorkoutComplete(on:) backed by workoutDates. - DayTimelineView takes workoutDone and uses it instead of live sets; Calendar passes the selected date's state, Today passes today's. - A date now shows complete only if that specific date was finished. Log KC-10 in ISSUES.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -309,3 +309,33 @@ actually logged on a past day.
|
|||||||
newest-first with program, sets done, and per-exercise breakdown.
|
newest-first with program, sets done, and per-exercise breakdown.
|
||||||
|
|
||||||
Files: `ExerciseModels.swift`, `WorkoutView.swift`.
|
Files: `ExerciseModels.swift`, `WorkoutView.swift`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KC-10 — Calendar marks future workouts complete (per-type, not per-date)
|
||||||
|
|
||||||
|
**Status:** In Progress (implemented & build-verified, pending device build)
|
||||||
|
**Reported by:** User
|
||||||
|
**Area:** Calendar / Workout
|
||||||
|
|
||||||
|
### Description
|
||||||
|
In the calendar, completing one occurrence of a program (e.g. Tuesday Shoulders)
|
||||||
|
showed **every** occurrence of that program as completed — including **future**
|
||||||
|
days (Saturday Shoulders) — with a checkmark + strikethrough.
|
||||||
|
|
||||||
|
### Root cause
|
||||||
|
`DayTimelineView` computed the workout's completion from the program's **live**
|
||||||
|
shared set state: `w.doneSets == w.totalSets`. `program(for: date)` returns the
|
||||||
|
same program object for every date it's scheduled, so its in-progress completion
|
||||||
|
leaked onto all of those dates.
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
- Added `WorkoutViewModel.isWorkoutComplete(on:)` — checks `workoutDates` (the set
|
||||||
|
of dates a workout was actually finished/Health-detected), i.e. **per-date**.
|
||||||
|
- `DayTimelineView` now takes a `workoutDone: Bool` and uses it instead of the live
|
||||||
|
`doneSets`. Calendar passes `isWorkoutComplete(on: selectedDate)`, Today passes
|
||||||
|
`isWorkoutComplete(on: today)`.
|
||||||
|
- Result: a date shows the workout complete only if that **specific date** was
|
||||||
|
finished. Future occurrences are never auto-completed.
|
||||||
|
|
||||||
|
Files: `ExerciseModels.swift`, `CalendarView.swift`, `TodayView.swift`.
|
||||||
|
|||||||
@@ -485,6 +485,13 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
/// or detected from Apple Health (both land in `workoutDates`).
|
/// or detected from Apple Health (both land in `workoutDates`).
|
||||||
var isTodayRecorded: Bool { workoutDates.contains(iso.string(from: Date())) }
|
var isTodayRecorded: Bool { workoutDates.contains(iso.string(from: Date())) }
|
||||||
|
|
||||||
|
/// Whether a workout was actually completed on a specific calendar date.
|
||||||
|
/// Per-date (from `workoutDates`), NOT the program's live set state — so
|
||||||
|
/// finishing one occurrence never marks other days of the same program.
|
||||||
|
func isWorkoutComplete(on date: Date) -> Bool {
|
||||||
|
workoutDates.contains(iso.string(from: date))
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Program management
|
// MARK: - Program management
|
||||||
func switchProgram(to id: UUID) {
|
func switchProgram(to id: UUID) {
|
||||||
syncBack()
|
syncBack()
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ struct CalendarView: View {
|
|||||||
},
|
},
|
||||||
calEvents: calStore.isAuthorized ? calStore.events(for: selectedDate) : [],
|
calEvents: calStore.isAuthorized ? calStore.events(for: selectedDate) : [],
|
||||||
workout: workoutVM.program(for: selectedDate),
|
workout: workoutVM.program(for: selectedDate),
|
||||||
|
workoutDone: workoutVM.isWorkoutComplete(on: selectedDate),
|
||||||
onWorkoutTap: {
|
onWorkoutTap: {
|
||||||
if let w = workoutVM.program(for: selectedDate),
|
if let w = workoutVM.program(for: selectedDate),
|
||||||
workoutVM.activeProgramId != w.id { workoutVM.switchProgram(to: w.id) }
|
workoutVM.activeProgramId != w.id { workoutVM.switchProgram(to: w.id) }
|
||||||
@@ -1538,6 +1539,7 @@ struct DayTimelineView: View {
|
|||||||
var overdueTasks: [TaskItem] = []
|
var overdueTasks: [TaskItem] = []
|
||||||
let calEvents: [EKEvent]
|
let calEvents: [EKEvent]
|
||||||
let workout: WorkoutProgram?
|
let workout: WorkoutProgram?
|
||||||
|
var workoutDone: Bool = false // per-DATE completion (from workoutDates), not live sets
|
||||||
var onWorkoutTap: (() -> Void)? = nil
|
var onWorkoutTap: (() -> Void)? = nil
|
||||||
var onToggle: ((TaskItem) -> Void)? = nil
|
var onToggle: ((TaskItem) -> Void)? = nil
|
||||||
var onPin: ((TaskItem) -> Void)? = nil
|
var onPin: ((TaskItem) -> Void)? = nil
|
||||||
@@ -1583,9 +1585,10 @@ struct DayTimelineView: View {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workout — always pinned first (sortGroup -1)
|
// Workout — always pinned first (sortGroup -1). Completion is tied to THIS
|
||||||
|
// date (workoutDates), never the program's shared live set state.
|
||||||
if let w = workout {
|
if let w = workout {
|
||||||
let done = w.doneSets == w.totalSets && w.totalSets > 0
|
let done = workoutDone
|
||||||
out.append(DayTLEntry(
|
out.append(DayTLEntry(
|
||||||
timeLabel: "Workout",
|
timeLabel: "Workout",
|
||||||
title: w.name,
|
title: w.name,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ struct TodayView: View {
|
|||||||
overdueTasks: [],
|
overdueTasks: [],
|
||||||
calEvents: todayCalEvents,
|
calEvents: todayCalEvents,
|
||||||
workout: workoutVM.program(for: Date()),
|
workout: workoutVM.program(for: Date()),
|
||||||
|
workoutDone: workoutVM.isWorkoutComplete(on: Date()),
|
||||||
onWorkoutTap: {
|
onWorkoutTap: {
|
||||||
if let w = workoutVM.program(for: Date()), workoutVM.activeProgramId != w.id {
|
if let w = workoutVM.program(for: Date()), workoutVM.activeProgramId != w.id {
|
||||||
workoutVM.switchProgram(to: w.id)
|
workoutVM.switchProgram(to: w.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user