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
+2 -3
View File
@@ -376,7 +376,6 @@ struct ScheduleDocument: Codable, Sendable, Equatable, Identifiable {
var goal: String? // GoalKind raw value; nil = unassigned
var recurrence: String // ScheduleRecurrence raw value
var weekdays: [Int]? // Calendar weekday numbers 1 (Sun) 7 (Sat); .fixedDays only
var timesPerWeek: Int? // .frequency only
var date: Date? = nil // the one-off day; .once only
var order: Int
var createdAt: Date
@@ -394,9 +393,9 @@ extension ScheduleDocument {
/// The recurrence, defaulting to `.daily` on an unknown raw value.
var recurrenceEnum: ScheduleRecurrence { ScheduleRecurrence(rawValue: recurrence) ?? .daily }
/// Human-readable recurrence line ("Jul 12, 2026" / "Daily" / "Mon & Thu" / "2× per week").
/// Human-readable recurrence line ("Jul 12, 2026" / "Daily" / "Mon & Thu").
var recurrenceSummary: String {
recurrenceEnum.summary(weekdays: weekdays, timesPerWeek: timesPerWeek, date: date)
recurrenceEnum.summary(weekdays: weekdays, date: date)
}
}
+3 -5
View File
@@ -317,7 +317,6 @@ final class Schedule {
var goalRaw: String?
var recurrenceRaw: String = ScheduleRecurrence.daily.rawValue
var weekdays: [Int]?
var timesPerWeek: Int?
var date: Date?
var order: Int = 0
var createdAt: Date = Date()
@@ -325,7 +324,7 @@ final class Schedule {
var jsonRelativePath: String = ""
init(id: String, routineID: String, routineName: String, goalRaw: String?,
recurrenceRaw: String, weekdays: [Int]?, timesPerWeek: Int?, date: Date? = nil,
recurrenceRaw: String, weekdays: [Int]?, date: Date? = nil,
order: Int, createdAt: Date, updatedAt: Date, jsonRelativePath: String) {
self.id = id
self.routineID = routineID
@@ -333,7 +332,6 @@ final class Schedule {
self.goalRaw = goalRaw
self.recurrenceRaw = recurrenceRaw
self.weekdays = weekdays
self.timesPerWeek = timesPerWeek
self.date = date
self.order = order
self.createdAt = createdAt
@@ -352,8 +350,8 @@ final class Schedule {
}
/// Human-readable recurrence line shares its formatting with `ScheduleDocument`
/// via `ScheduleRecurrence.summary(weekdays:timesPerWeek:date:)`.
/// via `ScheduleRecurrence.summary(weekdays:date:)`.
var recurrenceSummary: String {
recurrenceEnum.summary(weekdays: weekdays, timesPerWeek: timesPerWeek, date: date)
recurrenceEnum.summary(weekdays: weekdays, date: date)
}
}
+5 -8
View File
@@ -198,14 +198,13 @@ extension GoalKind {
/// How often a schedule expects its routine. Raw values are persisted in ScheduleDocument.
enum ScheduleRecurrence: String, CaseIterable, Codable, Sendable {
case once, daily, fixedDays, frequency
case once, daily, fixedDays
var displayName: String {
switch self {
case .once: "Once"
case .daily: "Daily"
case .fixedDays: "Fixed Days"
case .frequency: "Times per Week"
}
}
}
@@ -213,10 +212,10 @@ enum ScheduleRecurrence: String, CaseIterable, Codable, Sendable {
extension ScheduleRecurrence {
/// Human-readable recurrence description shared by `ScheduleDocument` and the
/// `Schedule` cache entity, so both render identically. `weekdays` uses Calendar
/// weekday numbers (1 = Sun 7 = Sat); `timesPerWeek` is the frequency count;
/// `date` is the one-off day (`.once` only). A degenerate case (fixed days with no
/// weekdays, a nil count or date) falls back to the recurrence's own `displayName`.
func summary(weekdays: [Int]?, timesPerWeek: Int?, date: Date?) -> String {
/// weekday numbers (1 = Sun 7 = Sat); `date` is the one-off day (`.once` only).
/// A degenerate case (fixed days with no weekdays, a nil date) falls back to the
/// recurrence's own `displayName`.
func summary(weekdays: [Int]?, date: Date?) -> String {
switch self {
case .once:
return date?.formatDate() ?? displayName // "Jul 12, 2026" / "Once"
@@ -230,8 +229,6 @@ extension ScheduleRecurrence {
(1...7).contains(n) ? symbols[n - 1] : nil
}
return names.isEmpty ? displayName : Self.joined(names)
case .frequency:
return "\(timesPerWeek ?? 0)× per week"
}
}
+2 -3
View File
@@ -91,7 +91,7 @@ extension ScheduleDocument {
self.init(schemaVersion: Self.currentSchemaVersion, id: schedule.id,
routineID: schedule.routineID, routineName: schedule.routineName,
goal: schedule.goalRaw, recurrence: schedule.recurrenceRaw,
weekdays: schedule.weekdays, timesPerWeek: schedule.timesPerWeek,
weekdays: schedule.weekdays,
date: schedule.date,
order: schedule.order, createdAt: schedule.createdAt, updatedAt: schedule.updatedAt)
}
@@ -255,7 +255,7 @@ enum CacheMapper {
} else {
schedule = Schedule(id: doc.id, routineID: doc.routineID, routineName: doc.routineName,
goalRaw: doc.goal, recurrenceRaw: doc.recurrence, weekdays: doc.weekdays,
timesPerWeek: doc.timesPerWeek, date: doc.date, order: doc.order,
date: doc.date, order: doc.order,
createdAt: doc.createdAt, updatedAt: doc.updatedAt,
jsonRelativePath: relativePath)
context.insert(schedule)
@@ -265,7 +265,6 @@ enum CacheMapper {
schedule.goalRaw = doc.goal
schedule.recurrenceRaw = doc.recurrence
schedule.weekdays = doc.weekdays
schedule.timesPerWeek = doc.timesPerWeek
schedule.date = doc.date
schedule.order = doc.order
schedule.createdAt = doc.createdAt