Workout: per-day history + log KC-8/KC-9
- Add per-day workout history: WorkoutDayLog/LoggedExercise/LoggedSet models stored at kisani.workout.history.<uid> (iCloud-synced). - snapshotDay(_:) captures the active program's completed sets on full completion and before the daily reset; only days with >=1 done set. - New WorkoutHistoryView (clock button in the Workout header) lists past days newest-first with per-exercise breakdown. - Log KC-8 (auto-switch / Rest Day) and KC-9 (history) in ISSUES.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,34 @@ struct WorkoutProgram: Identifiable, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Per-day Workout History
|
||||
|
||||
/// A snapshot of what was logged on a given day, captured before the daily reset.
|
||||
struct WorkoutDayLog: Identifiable, Codable {
|
||||
var id: String { date } // "yyyy-MM-dd"
|
||||
let date: String
|
||||
let programName: String
|
||||
let programEmoji: String
|
||||
let exercises: [LoggedExercise]
|
||||
|
||||
var totalSets: Int { exercises.reduce(0) { $0 + $1.sets.count } }
|
||||
var doneSets: Int { exercises.reduce(0) { $0 + $1.sets.filter(\.done).count } }
|
||||
}
|
||||
|
||||
struct LoggedExercise: Identifiable, Codable {
|
||||
var id = UUID()
|
||||
let name: String
|
||||
let sfSymbol: String
|
||||
let sets: [LoggedSet]
|
||||
var doneSets: Int { sets.filter(\.done).count }
|
||||
}
|
||||
|
||||
struct LoggedSet: Codable {
|
||||
let weight: Double
|
||||
let reps: Int
|
||||
let done: Bool
|
||||
}
|
||||
|
||||
// MARK: - Exercise Template Library
|
||||
enum ExerciseMuscle: String, CaseIterable, Identifiable {
|
||||
var id: String { rawValue }
|
||||
@@ -177,6 +205,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
@Published var activeProgramId: UUID
|
||||
@Published var activeSections: [WorkoutSection]
|
||||
@Published var workoutDates: [String] = []
|
||||
@Published var history: [WorkoutDayLog] = []
|
||||
@Published var restTimerSeconds: Int = 60
|
||||
@Published var restTimerEnabled: Bool = false
|
||||
@Published var restCountdown: Int = 0
|
||||
@@ -209,6 +238,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
private let kActivePid: String
|
||||
private let kWorkoutDates: String
|
||||
private let kLastActiveDay: String
|
||||
private let kWorkoutHistory: String
|
||||
|
||||
init() {
|
||||
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||
@@ -217,6 +247,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
||||
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
|
||||
self.kLastActiveDay = "kisani.workout.lastActiveDay.\(uid)"
|
||||
self.kWorkoutHistory = "kisani.workout.history.\(uid)"
|
||||
|
||||
// Restore all workout keys from iCloud if missing locally (fresh install / new build)
|
||||
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.programs.\(uid)")
|
||||
@@ -286,6 +317,10 @@ class WorkoutViewModel: ObservableObject {
|
||||
activeSections = blank.sections
|
||||
}
|
||||
workoutDates = UserDefaults.kisani.stringArray(forKey: kWorkoutDates) ?? []
|
||||
if let hData = UserDefaults.kisani.data(forKey: kWorkoutHistory),
|
||||
let saved = try? JSONDecoder().decode([WorkoutDayLog].self, from: hData) {
|
||||
history = saved
|
||||
}
|
||||
resetSetsForNewDayIfNeeded()
|
||||
}
|
||||
|
||||
@@ -298,6 +333,10 @@ class WorkoutViewModel: ObservableObject {
|
||||
UserDefaults.kisani.set(schedDict, forKey: kSchedule)
|
||||
UserDefaults.kisani.set(activeProgramId.uuidString, forKey: kActivePid)
|
||||
UserDefaults.kisani.set(workoutDates, forKey: kWorkoutDates)
|
||||
if let hData = try? JSONEncoder().encode(history) {
|
||||
UserDefaults.kisani.set(hData, forKey: kWorkoutHistory)
|
||||
CloudSyncManager.shared.push(data: hData, forKey: kWorkoutHistory)
|
||||
}
|
||||
CloudSyncManager.shared.push(value: schedDict, forKey: kSchedule)
|
||||
CloudSyncManager.shared.push(value: activeProgramId.uuidString, forKey: kActivePid)
|
||||
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
|
||||
@@ -313,13 +352,36 @@ class WorkoutViewModel: ObservableObject {
|
||||
let today = iso.string(from: Date())
|
||||
let last = UserDefaults.kisani.string(forKey: kLastActiveDay)
|
||||
guard last != today else { return }
|
||||
if last != nil {
|
||||
clearAllDoneSets() // skip the wipe on the very first launch
|
||||
if let last = last { // skip on the very first launch
|
||||
snapshotDay(last) // save what was logged before wiping it
|
||||
clearAllDoneSets()
|
||||
applyScheduledProgramForToday() // follow the weekly schedule on a new day
|
||||
}
|
||||
UserDefaults.kisani.set(today, forKey: kLastActiveDay)
|
||||
}
|
||||
|
||||
/// History newest-first, for the history view.
|
||||
var historySorted: [WorkoutDayLog] { history.sorted { $0.date > $1.date } }
|
||||
|
||||
/// Capture the active program's completed work for `dayKey` into history.
|
||||
/// Only records days where at least one set was done; updates in place.
|
||||
func snapshotDay(_ dayKey: String) {
|
||||
let logged: [LoggedExercise] = activeSections.flatMap { section in
|
||||
section.exercises.map { ex in
|
||||
LoggedExercise(name: ex.name, sfSymbol: ex.sfSymbol,
|
||||
sets: ex.sets.map { LoggedSet(weight: $0.weight, reps: $0.reps, done: $0.isDone) })
|
||||
}
|
||||
}
|
||||
guard logged.contains(where: { $0.doneSets > 0 }) else { return }
|
||||
let log = WorkoutDayLog(date: dayKey,
|
||||
programName: activeProgram?.name ?? "Workout",
|
||||
programEmoji: activeProgram?.emoji ?? "dumbbell.fill",
|
||||
exercises: logged)
|
||||
history.removeAll { $0.date == dayKey }
|
||||
history.append(log)
|
||||
save()
|
||||
}
|
||||
|
||||
/// The program assigned to today in the weekly schedule (nil = rest day).
|
||||
var todaysScheduledProgram: WorkoutProgram? { program(for: Date()) }
|
||||
|
||||
@@ -357,6 +419,7 @@ class WorkoutViewModel: ObservableObject {
|
||||
|
||||
func logWorkoutCompleted(on date: Date = Date()) {
|
||||
let key = iso.string(from: date)
|
||||
snapshotDay(key) // record today's session in history
|
||||
guard !workoutDates.contains(key) else { return }
|
||||
workoutDates.append(key)
|
||||
save()
|
||||
|
||||
Reference in New Issue
Block a user