import IndieSync import SwiftUI import SwiftData /// Add or edit a schedule: pick a routine, choose a recurrence (once / daily / fixed /// days), tag it with a goal, and optionally set a reminder time. Mac-native sheet /// (grouped Form, fixed width, Cancel/Save) — mirrors iOS `ScheduleAddEditView` minus /// the "Now" start-immediately choice (the Mac never runs a workout) and the /// notification-permission footer (the Mac never schedules a notification; the /// iPhone's `ReminderScheduler` resyncs once it observes this document). Fields are /// snapshotted in `init` so the sheet never holds the live entity. struct MacScheduleEditorSheet: View { @Environment(SyncEngine.self) private var syncEngine @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. Mirrors iOS. @State private var originalRoutineName: String @State private var goal: GoalKind? @State private var recurrence: ScheduleRecurrence @State private var weekdays: Set @State private var date: Date @State private var reminderEnabled: Bool @State private var reminderTime: Date init(schedule: Schedule?) { _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 ?? [])) _date = State(initialValue: schedule?.date ?? Date()) _reminderEnabled = State(initialValue: schedule?.reminderMinutes != nil) _reminderTime = State(initialValue: ScheduleEditing.time(fromMinutes: schedule?.reminderMinutes)) } private var isEditing: Bool { scheduleID != nil } /// The live routine currently selected, following the seed clone-on-edit redirect /// (a schedule made against a starter seed keeps the retired seed id after the /// routine is edited); nil when the routine has been deleted. Mirrors iOS /// `ScheduleAddEditView.selectedRoutine`. private var selectedRoutine: Routine? { guard !routineID.isEmpty else { return nil } let liveID = syncEngine.currentRoutineID(for: routineID) return routines.first { $0.id == liveID } } 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 recurrenceSection goalSection reminderSection } .formStyle(.grouped) .navigationTitle(isEditing ? "Edit Schedule" : "New Schedule") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { 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 ?? "" } } } .frame(width: 460, height: 560) } // MARK: - Sections private var routineSection: some View { Section { Picker("Routine", selection: routineSelectionBinding) { if selectedRoutine == nil { Text("Please select").tag("") } ForEach(routines) { routine in Label(routine.name, systemImage: routine.systemImage) .tag(routine.id) } } .labelsHidden() } header: { Text("Routine") } footer: { if selectedRoutine == nil, !originalRoutineName.isEmpty { Text("\"\(originalRoutineName)\" is no longer available. Choose a routine.") .font(.caption) .foregroundStyle(.secondary) } } } /// The Picker always shows the *resolved* current selection (following a /// clone-on-edit redirect), but writes back the raw id the user picked — save() /// re-resolves through `selectedRoutine` regardless. private var routineSelectionBinding: Binding { Binding( get: { selectedRoutine?.id ?? "" }, set: { routineID = $0 } ) } @ViewBuilder private var recurrenceSection: some View { Section { Picker("When", selection: $recurrence) { ForEach(ScheduleRecurrence.allCases, id: \.self) { r in Text(r.displayName).tag(r) } } .pickerStyle(.segmented) .labelsHidden() switch recurrence { case .daily: EmptyView() case .once: DatePicker("Date", selection: $date, displayedComponents: .date) .datePickerStyle(.field) case .fixedDays: weekdayPicker } } header: { Text("When") } } 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)) } } } header: { Text("Goal") } } private var reminderSection: some View { Section { Toggle("Remind Me", isOn: $reminderEnabled) if reminderEnabled { DatePicker("Time", selection: $reminderTime, displayedComponents: .hourAndMinute) .datePickerStyle(.field) } } header: { Text("Reminder") } footer: { Text("The Mac doesn't send reminders itself — your iPhone schedules the notification once it syncs this schedule.") .font(.caption) .foregroundStyle(.secondary) } } /// A row of toggleable weekday chips, Monday-first. Mirrors iOS /// `ScheduleAddEditView.weekdayPicker`. private var weekdayPicker: some View { HStack(spacing: 6) { ForEach(ScheduleEditing.weekdayOrder, id: \.self) { n in let on = weekdays.contains(n) Button { if on { weekdays.remove(n) } else { weekdays.insert(n) } } label: { Text(ScheduleEditing.weekdaySymbols[n - 1]) .font(.caption.weight(.semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 6) .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, date: recurrence == .once ? date : nil, reminderMinutes: reminderEnabled ? ScheduleEditing.minutesFromMidnight(of: reminderTime) : nil, order: isEditing ? order : ScheduleEditing.nextOrder(in: modelContext), createdAt: createdAt, updatedAt: Date() ) Task { await syncEngine.save(schedule: doc) } dismiss() } }