// // ScheduleAddEditView.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import IndieSync import SwiftUI import SwiftData /// Add / edit a schedule: pick a routine, tag it with a goal, and set how often it /// recurs. Fields are snapshotted in `init` so the sheet never holds the live entity. struct ScheduleAddEditView: View { @Environment(SyncEngine.self) private var sync @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss @Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)]) private var routines: [Routine] /// nil id → add mode; a value → edit mode (id / order / createdAt preserved). @State private var scheduleID: String? @State private var order: Int @State private var createdAt: Date @State private var routineID: String /// The schedule's routine name when it was created — surfaced when that routine /// has since been deleted so the user knows what they're reassigning. @State private var originalRoutineName: String @State private var goal: GoalKind? @State private var recurrence: ScheduleRecurrence @State private var weekdays: Set @State private var timesPerWeek: Int @State private var date: Date /// Calendar weekday numbers in Monday-first display order (Mon…Sat = 2…7, Sun = 1). private let weekdayOrder = [2, 3, 4, 5, 6, 7, 1] /// Gregorian short names, 0-indexed from Sunday — weekday `n` is `symbols[n - 1]`. private let weekdaySymbols = Calendar(identifier: .gregorian).shortWeekdaySymbols /// `defaultDate` seeds the `.once` day when adding — the Today board passes the day /// currently on screen, so "navigate to a day, tap +" plans a one-off for that day. init(schedule: Schedule? = nil, defaultDate: Date = Date()) { _scheduleID = State(initialValue: schedule?.id) _order = State(initialValue: schedule?.order ?? 0) _createdAt = State(initialValue: schedule?.createdAt ?? Date()) _routineID = State(initialValue: schedule?.routineID ?? "") _originalRoutineName = State(initialValue: schedule?.routineName ?? "") _goal = State(initialValue: schedule?.goalKind) _recurrence = State(initialValue: schedule?.recurrenceEnum ?? .once) _weekdays = State(initialValue: Set(schedule?.weekdays ?? [])) _timesPerWeek = State(initialValue: schedule?.timesPerWeek ?? 3) _date = State(initialValue: schedule?.date ?? defaultDate) } private var isEditing: Bool { scheduleID != nil } /// The live routine currently selected; nil when the stored id no longer resolves. private var selectedRoutine: Routine? { routines.first { $0.id == routineID } } private var canSave: Bool { guard selectedRoutine != nil else { return false } if recurrence == .fixedDays { return !weekdays.isEmpty } return true } var body: some View { NavigationStack { Form { routineSection goalSection recurrenceSection } .navigationTitle(isEditing ? "Edit Schedule" : "New Schedule") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save", action: save).disabled(!canSave) } } .onAppear { // Default to the first routine when adding (the @Query isn't populated at init). if routineID.isEmpty { routineID = routines.first?.id ?? "" } } } } // MARK: - Sections private var routineSection: some View { Section { Picker("Routine", selection: $routineID) { ForEach(routines) { routine in Text(routine.name).tag(routine.id) } } } header: { Text("Routine") } footer: { if selectedRoutine == nil, !originalRoutineName.isEmpty { Text("\"\(originalRoutineName)\" is no longer available. Choose a routine.") } } } private var goalSection: some View { Section { Picker("Goal", selection: $goal) { Text("None").tag(GoalKind?.none) ForEach(GoalKind.orderedCases()) { kind in Label(kind.displayName, systemImage: kind.systemImage) .foregroundStyle(Color.color(from: kind.colorName)) .tag(GoalKind?.some(kind)) } } .pickerStyle(.inline) .labelsHidden() } header: { Text("Goal") } } @ViewBuilder private var recurrenceSection: some View { Section { Picker("Repeat", selection: $recurrence) { ForEach(ScheduleRecurrence.allCases, id: \.self) { r in Text(r.displayName).tag(r) } } switch recurrence { case .once: DatePicker("Date", selection: $date, displayedComponents: .date) case .daily: EmptyView() case .fixedDays: weekdayPicker case .frequency: Stepper(value: $timesPerWeek, in: 1...7) { Text("\(timesPerWeek)× per week") } } } header: { Text("Repeat") } } /// A row of toggleable weekday chips, Monday-first. private var weekdayPicker: some View { HStack(spacing: 6) { ForEach(weekdayOrder, id: \.self) { n in let on = weekdays.contains(n) Button { if on { weekdays.remove(n) } else { weekdays.insert(n) } } label: { Text(weekdaySymbols[n - 1]) .font(.caption.weight(.semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 8) .background(on ? Color.accentColor : Color.secondary.opacity(0.15), in: Capsule()) .foregroundStyle(on ? Color.white : Color.primary) } .buttonStyle(.plain) } } .padding(.vertical, 4) } // MARK: - Save private func save() { guard let routine = selectedRoutine else { return } let doc = ScheduleDocument( schemaVersion: ScheduleDocument.currentSchemaVersion, id: scheduleID ?? ULID.make(), routineID: routine.id, routineName: routine.name, goal: goal?.rawValue, recurrence: recurrence.rawValue, weekdays: recurrence == .fixedDays ? weekdays.sorted() : nil, timesPerWeek: recurrence == .frequency ? timesPerWeek : nil, date: recurrence == .once ? date : nil, order: isEditing ? order : nextOrder(), createdAt: createdAt, updatedAt: Date() ) Task { await sync.save(schedule: doc) } dismiss() } /// One past the highest existing schedule order — appends a new schedule to the end. private func nextOrder() -> Int { let existing = (try? modelContext.fetch(FetchDescriptor())) ?? [] return (existing.map(\.order).max() ?? -1) + 1 } }