File ad hoc workouts under their routine's goal on the Today board
An ad hoc workout was only launched ad hoc — any schedule referencing the same routine (through the clone redirect) still says what the user trains it for. Infer the goal from those schedules, tie-breaking by the user's goal order; only truly unassigned workouts stay in Unassigned. Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
This commit is contained in:
@@ -63,7 +63,8 @@ struct TodayView: View {
|
||||
/// Workouts on the viewed day whose routine isn't already shown via a schedule row
|
||||
/// above — e.g. an ad hoc "Start Now" workout, or a routine whose schedule isn't due
|
||||
/// today. Matched the same way `todaysWorkout` matches a workout to a schedule: both
|
||||
/// sides live-resolved through the seed clone-on-edit redirect.
|
||||
/// sides live-resolved through the seed clone-on-edit redirect. Each is then filed
|
||||
/// under its `inferredGoal(for:)` group, falling back to Unassigned.
|
||||
private var orphanWorkouts: [Workout] {
|
||||
let scheduledRoutineIDs = Set(visibleSchedules.map { sync.currentRoutineID(for: $0.routineID) })
|
||||
return workouts.filter { workout in
|
||||
@@ -73,11 +74,16 @@ struct TodayView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-empty goal groups in the user's chosen goal order.
|
||||
private var goalGroups: [(kind: GoalKind, schedules: [Schedule])] {
|
||||
GoalKind.orderedCases().compactMap { kind in
|
||||
/// Non-empty goal groups in the user's chosen goal order. A group exists when the
|
||||
/// viewed day has due schedules for that goal, orphan workouts inferred into it
|
||||
/// (see `inferredGoal(for:)`), or both — so an ad hoc strength run surfaces a
|
||||
/// Strength header even on a day with no strength schedule due.
|
||||
private var goalGroups: [(kind: GoalKind, schedules: [Schedule], orphans: [Workout])] {
|
||||
let orphansByGoal = Dictionary(grouping: orphanWorkouts) { inferredGoal(for: $0) }
|
||||
return GoalKind.orderedCases().compactMap { kind in
|
||||
let matches = visibleSchedules.filter { $0.goalKind == kind }
|
||||
return matches.isEmpty ? nil : (kind, matches)
|
||||
let orphans = orphansByGoal[kind] ?? []
|
||||
return (matches.isEmpty && orphans.isEmpty) ? nil : (kind, matches, orphans)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +92,28 @@ struct TodayView: View {
|
||||
visibleSchedules.filter { $0.goalKind == nil }
|
||||
}
|
||||
|
||||
/// Orphan workouts whose routine no schedule anywhere assigns a goal.
|
||||
private var unassignedOrphanWorkouts: [Workout] {
|
||||
orphanWorkouts.filter { inferredGoal(for: $0) == nil }
|
||||
}
|
||||
|
||||
/// The goal an orphan workout belongs to: the goal of any schedule — due today or
|
||||
/// not — referencing the same routine (through the clone redirect on both sides).
|
||||
/// The workout was "ad hoc" only in how it was launched; the routine's schedule
|
||||
/// still tells us what the user trains it for. Multiple schedules with different
|
||||
/// goals tie-break by the user's goal order. Nil = truly unassigned.
|
||||
private func inferredGoal(for workout: Workout) -> GoalKind? {
|
||||
guard let rid = workout.routineID else { return nil }
|
||||
let liveID = sync.currentRoutineID(for: rid)
|
||||
let goals = schedules
|
||||
.filter { sync.currentRoutineID(for: $0.routineID) == liveID }
|
||||
.compactMap(\.goalKind)
|
||||
let order = GoalKind.orderedCases()
|
||||
return goals.min {
|
||||
(order.firstIndex(of: $0) ?? .max) < (order.firstIndex(of: $1) ?? .max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Motivational openers for the board title — each reads as a phrase completed
|
||||
/// by "Today" ("Seize Today"). One per day, rotating by day-of-era: stable all
|
||||
/// day, fresh the next.
|
||||
@@ -115,16 +143,17 @@ struct TodayView: View {
|
||||
ForEach(goalGroups, id: \.kind) { group in
|
||||
Section {
|
||||
ForEach(group.schedules) { scheduleRow($0) }
|
||||
ForEach(group.orphans) { orphanWorkoutRow($0) }
|
||||
} header: {
|
||||
Label(group.kind.displayName, systemImage: group.kind.systemImage)
|
||||
.foregroundStyle(Color.color(from: group.kind.colorName))
|
||||
}
|
||||
}
|
||||
|
||||
if !unassignedSchedules.isEmpty || !orphanWorkouts.isEmpty {
|
||||
if !unassignedSchedules.isEmpty || !unassignedOrphanWorkouts.isEmpty {
|
||||
Section("Unassigned") {
|
||||
ForEach(unassignedSchedules) { scheduleRow($0) }
|
||||
ForEach(orphanWorkouts) { orphanWorkoutRow($0) }
|
||||
ForEach(unassignedOrphanWorkouts) { orphanWorkoutRow($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user