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
|
/// 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
|
/// 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
|
/// 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] {
|
private var orphanWorkouts: [Workout] {
|
||||||
let scheduledRoutineIDs = Set(visibleSchedules.map { sync.currentRoutineID(for: $0.routineID) })
|
let scheduledRoutineIDs = Set(visibleSchedules.map { sync.currentRoutineID(for: $0.routineID) })
|
||||||
return workouts.filter { workout in
|
return workouts.filter { workout in
|
||||||
@@ -73,11 +74,16 @@ struct TodayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-empty goal groups in the user's chosen goal order.
|
/// Non-empty goal groups in the user's chosen goal order. A group exists when the
|
||||||
private var goalGroups: [(kind: GoalKind, schedules: [Schedule])] {
|
/// viewed day has due schedules for that goal, orphan workouts inferred into it
|
||||||
GoalKind.orderedCases().compactMap { kind in
|
/// (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 }
|
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 }
|
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
|
/// 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
|
/// by "Today" ("Seize Today"). One per day, rotating by day-of-era: stable all
|
||||||
/// day, fresh the next.
|
/// day, fresh the next.
|
||||||
@@ -115,16 +143,17 @@ struct TodayView: View {
|
|||||||
ForEach(goalGroups, id: \.kind) { group in
|
ForEach(goalGroups, id: \.kind) { group in
|
||||||
Section {
|
Section {
|
||||||
ForEach(group.schedules) { scheduleRow($0) }
|
ForEach(group.schedules) { scheduleRow($0) }
|
||||||
|
ForEach(group.orphans) { orphanWorkoutRow($0) }
|
||||||
} header: {
|
} header: {
|
||||||
Label(group.kind.displayName, systemImage: group.kind.systemImage)
|
Label(group.kind.displayName, systemImage: group.kind.systemImage)
|
||||||
.foregroundStyle(Color.color(from: group.kind.colorName))
|
.foregroundStyle(Color.color(from: group.kind.colorName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !unassignedSchedules.isEmpty || !orphanWorkouts.isEmpty {
|
if !unassignedSchedules.isEmpty || !unassignedOrphanWorkouts.isEmpty {
|
||||||
Section("Unassigned") {
|
Section("Unassigned") {
|
||||||
ForEach(unassignedSchedules) { scheduleRow($0) }
|
ForEach(unassignedSchedules) { scheduleRow($0) }
|
||||||
ForEach(orphanWorkouts) { orphanWorkoutRow($0) }
|
ForEach(unassignedOrphanWorkouts) { orphanWorkoutRow($0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user