Show only the day's docket on the Today board and add ad hoc Now workouts

- Due-filter schedules: daily always, fixed days by weekday, one-offs on
  their date; rest days get an empty state
- Workouts with no due schedule row (ad hoc or off-day starts) now render
  as their own board rows
- The + sheet is now "New Workout" with a "When" picker; "Now" (offered
  when adding on today) skips the schedule and starts the workout directly
- Remove the times-per-week scheduling mode everywhere (enum, document,
  entity, mappers, planner, seeds, tests)
This commit is contained in:
2026-07-11 11:09:11 -04:00
parent 9dc9283b4c
commit 7cb2d6da26
14 changed files with 231 additions and 199 deletions
+3 -11
View File
@@ -24,7 +24,6 @@ struct ScheduleFacts: Sendable, Equatable {
var goal: GoalKind?
var recurrence: ScheduleRecurrence
var weekdays: [Int]? // Calendar weekday numbers 1 (Sun) 7 (Sat); .fixedDays only
var timesPerWeek: Int? // .frequency only
var onceDate: Date? // .once only
var createdAt: Date
var routineID: String // live-resolved
@@ -69,8 +68,8 @@ struct WeekMark: Sendable, Equatable {
struct ScheduleTrack: Sendable, Equatable {
var schedule: ScheduleFacts
var weeks: [WeekMark?]
/// The current week can no longer be met (a daily's missed day, too few days left
/// for the remaining frequency count). Distinct from "not yet hit".
/// The current week can no longer be met (a daily's missed day, or too few days
/// left to cover a fixed day already missed). Distinct from "not yet hit".
var currentWeekBroken: Bool
var currentWeek: WeekMark? { weeks.last ?? nil }
@@ -224,8 +223,6 @@ enum ProgressPlanner {
/// - `.daily` / `.fixedDays`: expected counts only required days **up to today**;
/// met counts distinct completed days in that same window (so a Tuesday make-up
/// for a missed fixed Monday still counts catch-ups are honored).
/// - `.frequency`: expected is the full weekly target (clamped to the days the
/// schedule existed that week); mid-week it reads "1 of 2" until actually met.
/// - `.once`: expected 1, only in the week containing its date.
static func weekMark(
schedule: ScheduleFacts,
@@ -248,9 +245,6 @@ enum ProgressPlanner {
let required = window.elapsedDays
.filter { wanted.contains(calendar.component(.weekday, from: $0)) }.count
return WeekMark(weekStart: weekStart, met: min(done, required), expected: required)
case .frequency:
let target = min(max(schedule.timesPerWeek ?? 0, 0), window.fullDays.count)
return WeekMark(weekStart: weekStart, met: min(done, target), expected: target)
case .once:
guard let once = schedule.onceDate,
calendar.dateInterval(of: .weekOfYear, for: once)?.start == weekStart
@@ -261,7 +255,7 @@ enum ProgressPlanner {
/// True when the current week can no longer be met: the days remaining (today
/// included) are fewer than the requirement still outstanding. A daily's missed
/// yesterday breaks immediately; a 2×/week with one done and two days left doesn't.
/// yesterday breaks immediately; fixed days with catch-up days still left don't.
static func currentWeekBroken(
schedule: ScheduleFacts,
doneDays: Set<Date>,
@@ -285,8 +279,6 @@ enum ProgressPlanner {
let wanted = Set(schedule.weekdays ?? [])
fullRequirement = window.fullDays
.filter { wanted.contains(calendar.component(.weekday, from: $0)) }.count
case .frequency:
fullRequirement = min(max(schedule.timesPerWeek ?? 0, 0), window.fullDays.count)
case .once:
guard let once = schedule.onceDate,
calendar.dateInterval(of: .weekOfYear, for: once)?.start == weekStart