import Foundation import SwiftUI struct EditOverrideForm: View { @ObservedObject var override: OverrideStored @Environment(\.presentationMode) var presentationMode @Environment(\.colorScheme) var colorScheme @StateObject var state: OverrideConfig.StateModel @State private var name: String @State private var percentage: Double @State private var indefinite: Bool @State private var duration: Decimal @State private var target: Decimal? @State private var advancedSettings: Bool @State private var smbIsOff: Bool @State private var smbIsScheduledOff: Bool @State private var start: Decimal? @State private var end: Decimal? @State private var isfAndCr: Bool @State private var isf: Bool @State private var cr: Bool @State private var smbMinutes: Decimal? @State private var uamMinutes: Decimal? @State private var selectedIsfCrOption: isfAndOrCrOptions @State private var selectedDisableSmbOption: disableSmbOptions @State private var hasChanges = false @State private var isEditing = false @State private var target_override = false @State private var showAlert = false @State private var displayPickerDuration: Bool = false @State private var displayPickerStart: Bool = false @State private var displayPickerEnd: Bool = false @State private var displayPickerSmbMinutes: Bool = false @State private var displayPickerUamMinutes: Bool = false init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) { override = overrideToEdit _state = StateObject(wrappedValue: state) _name = State(initialValue: overrideToEdit.name ?? "") _percentage = State(initialValue: overrideToEdit.percentage) _indefinite = State(initialValue: overrideToEdit.indefinite) _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0) _target = State( initialValue: state.units == .mgdL ? overrideToEdit.target?.decimalValue : overrideToEdit.target? .decimalValue.asMmolL ) _target_override = State(initialValue: overrideToEdit.target?.decimalValue != 0) _advancedSettings = State(initialValue: overrideToEdit.advancedSettings) _smbIsOff = State(initialValue: overrideToEdit.smbIsOff) _smbIsScheduledOff = State(initialValue: overrideToEdit.smbIsScheduledOff) _start = State(initialValue: overrideToEdit.start?.decimalValue) _end = State(initialValue: overrideToEdit.end?.decimalValue) _isfAndCr = State(initialValue: overrideToEdit.isfAndCr) _isf = State(initialValue: overrideToEdit.isf) _cr = State(initialValue: overrideToEdit.cr) _selectedIsfCrOption = State( initialValue: overrideToEdit.isfAndCr ? .isfAndCr : (overrideToEdit.isf ? .isf : (overrideToEdit.cr ? .cr : .none)) ) _selectedDisableSmbOption = State( initialValue: overrideToEdit.smbIsScheduledOff ? .disableOnSchedule : (overrideToEdit.smbIsOff ? .disable : .dontDisable) ) _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue) _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue) } enum isfAndOrCrOptions: String, CaseIterable { case isfAndCr = "ISF/CR" case isf = "ISF" case cr = "CR" case none = "None" } enum disableSmbOptions: String, CaseIterable { case dontDisable = "Don't Disable" case disable = "Disable" case disableOnSchedule = "Disable on Schedule" } var color: LinearGradient { colorScheme == .dark ? LinearGradient( gradient: Gradient(colors: [ Color.bgDarkBlue, Color.bgDarkerDarkBlue ]), startPoint: .top, endPoint: .bottom ) : LinearGradient( gradient: Gradient(colors: [Color.gray.opacity(0.1)]), startPoint: .top, endPoint: .bottom ) } private var formatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 0 return formatter } private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 0 if state.units == .mmolL { formatter.maximumFractionDigits = 1 } formatter.roundingMode = .halfUp return formatter } var body: some View { NavigationView { Form { editOverride() saveButton }.scrollContentBackground(.hidden).background(color) .navigationTitle("Edit Override") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(leading: Button("Close") { presentationMode.wrappedValue.dismiss() }) .onDisappear { if !hasChanges { // Reset UI changes resetValues() } } .alert(isPresented: $state.showInvalidTargetAlert) { Alert( title: Text("Invalid Input"), message: Text("\(state.alertMessage)"), dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false } ) } } } @ViewBuilder private func editOverride() -> some View { Section { if override.name != nil { VStack { HStack { Text("Name") Spacer() TextField("Name", text: $name) .onChange(of: name) { _ in hasChanges = true } .multilineTextAlignment(.trailing) } } } VStack { HStack { Spacer() // Decrement button Button(action: { if percentage > 10 { percentage -= 1 } }) { Image(systemName: "minus.circle.fill") .font(.title) .foregroundColor(percentage > 10 ? .accentColor : .loopGray) } .buttonStyle(PlainButtonStyle()) Spacer() Text("\(percentage.formatted(.number)) %") .foregroundColor(.accentColor) .font(.largeTitle) Spacer() // Increment button Button(action: { if percentage < 200 { percentage += 1 } }) { Image(systemName: "plus.circle.fill") .font(.title) .foregroundColor(percentage < 200 ? .accentColor : .loopGray) } .buttonStyle(PlainButtonStyle()) Spacer() } .padding() Slider( value: $percentage, in: 10 ... 200, step: 1 ).onChange(of: percentage) { _ in hasChanges = true } // Picker for ISF/CR settings Picker("Apply to", selection: $selectedIsfCrOption) { ForEach(isfAndOrCrOptions.allCases, id: \.self) { option in Text(option.rawValue).tag(option) } } .pickerStyle(MenuPickerStyle()) .onChange(of: selectedIsfCrOption) { newValue in switch newValue { case .isfAndCr: isfAndCr = true isf = false cr = false case .isf: isfAndCr = false isf = true cr = false case .cr: isfAndCr = false isf = false cr = true case .none: isfAndCr = false isf = false cr = false } hasChanges = true } } VStack { Toggle(isOn: $indefinite) { Text("Enable Indefinitely") }.onChange(of: indefinite) { _ in hasChanges = true } if !indefinite { VStack { HStack { Text("Duration") Spacer() Text(formatHrMin(Int(truncating: duration as NSNumber))) .foregroundColor(!displayPickerDuration ? .primary : .accentColor) } .onTapGesture { displayPickerDuration.toggle() } if displayPickerDuration { HStack { Picker( selection: Binding( get: { Int(truncating: duration as NSNumber) / 60 }, set: { duration = Decimal($0 * 60 + Int(truncating: duration as NSNumber) % 60) hasChanges = true } ), label: Text("") ) { ForEach(0 ..< 24) { hour in Text("\(hour) hr").tag(hour) } } .pickerStyle(WheelPickerStyle()) .frame(width: 100) Picker( selection: Binding( get: { Int(truncating: duration as NSNumber) % 60 // Convert Decimal to Int for modulus operation }, set: { duration = Decimal((Int(truncating: duration as NSNumber) / 60) * 60 + $0) hasChanges = true } ), label: Text("") ) { ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in Text("\(minute) min").tag(minute) } } .pickerStyle(WheelPickerStyle()) .frame(width: 100) } } } .padding(.top) } } VStack { Toggle(isOn: $target_override) { Text("Override Target") }.onChange(of: target_override) { _ in hasChanges = true } if target_override { HStack { Text("Target Glucose") TextFieldWithToolBar(text: Binding( get: { target ?? 0 }, set: { target = $0 hasChanges = true } ), placeholder: "0", numberFormatter: glucoseFormatter) Text(state.units.rawValue).foregroundColor(.secondary) } } } VStack { // Picker for Disable SMB settings Picker("Disable SMBs", selection: $selectedDisableSmbOption) { ForEach(disableSmbOptions.allCases, id: \.self) { option in Text(option.rawValue).tag(option) } } .pickerStyle(MenuPickerStyle()) .onChange(of: selectedDisableSmbOption) { newValue in switch newValue { case .dontDisable: smbIsOff = false smbIsScheduledOff = false case .disable: smbIsOff = true smbIsScheduledOff = false case .disableOnSchedule: smbIsOff = false smbIsScheduledOff = true } hasChanges = true } if smbIsScheduledOff { // First Hour SMBs Are Disabled VStack { HStack { Text("From") Spacer() Text( is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" : convertTo12HourFormat(Int(truncating: start! as NSNumber)) ) .foregroundColor(!displayPickerStart ? .primary : .accentColor) } .onTapGesture { displayPickerStart.toggle() } if displayPickerStart { Picker(selection: Binding( get: { Int(truncating: start! as NSNumber) }, set: { start = Decimal($0) hasChanges = true } ), label: Text("")) { if is24HourFormat() { ForEach(0 ..< 24, id: \.self) { hour in Text(format24Hour(hour) + ":00").tag(hour) } } else { ForEach(0 ..< 24, id: \.self) { hour in Text(convertTo12HourFormat(hour)).tag(hour) } } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } } .padding(.top) // First Hour SMBs Are Resumed VStack { HStack { Text("To") Spacer() Text( is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" : convertTo12HourFormat(Int(truncating: end! as NSNumber)) ) .foregroundColor(!displayPickerEnd ? .primary : .accentColor) } .onTapGesture { displayPickerEnd.toggle() } if displayPickerEnd { Picker(selection: Binding( get: { Int(truncating: end! as NSNumber) }, set: { end = Decimal($0) hasChanges = true } ), label: Text("")) { if is24HourFormat() { ForEach(0 ..< 24, id: \.self) { hour in Text(format24Hour(hour) + ":00").tag(hour) } } else { ForEach(0 ..< 24, id: \.self) { hour in Text(convertTo12HourFormat(hour)).tag(hour) } } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } } .padding(.top) } } if !smbIsOff { VStack { Toggle(isOn: $advancedSettings) { Text("Change Max SMB Minutes") }.onChange(of: advancedSettings) { _ in hasChanges = true } if advancedSettings { // SMB Minutes Picker VStack { HStack { Text("Max SMB Minutes") Spacer() Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min") .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor) } .onTapGesture { displayPickerSmbMinutes.toggle() } if displayPickerSmbMinutes { Picker( selection: Binding( get: { smbMinutes ?? state.defaultSmbMinutes }, set: { smbMinutes = $0 hasChanges = true } ), label: Text("") ) { ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in Text("\(minute) min").tag(Decimal(minute)) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } } .padding(.top) // UAM SMB Minutes Picker VStack { HStack { Text("Max UAM SMB Minutes") Spacer() Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min") .foregroundColor(!displayPickerUamMinutes ? .primary : .accentColor) } .onTapGesture { displayPickerUamMinutes.toggle() } if displayPickerUamMinutes { Picker( selection: Binding( get: { uamMinutes ?? state.defaultUamMinutes }, set: { uamMinutes = $0 hasChanges = true } ), label: Text("") ) { ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in Text("\(minute) min").tag(Decimal(minute)) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } } .padding(.top) } } } }.listRowBackground(Color.chart) } private var saveButton: some View { HStack { Spacer() Button(action: { if !state.isInputInvalid(target: target ?? 0) { saveChanges() do { guard let moc = override.managedObjectContext else { return } guard moc.hasChanges else { return } try moc.save() if let currentActiveOverride = state.currentActiveOverride { Task { await state.disableAllActiveOverrides( except: currentActiveOverride.objectID, createOverrideRunEntry: false ) } } // Update View state.updateLatestOverrideConfiguration() hasChanges = false presentationMode.wrappedValue.dismiss() } catch { debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override") } } }, label: { Text("Save") }) .disabled(!hasChanges || (!indefinite && duration == 0)) .frame(maxWidth: .infinity, alignment: .center) .tint(.white) Spacer() }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4)) } private func saveChanges() { if !override.isPreset, hasChanges, name == (override.name ?? "") { override.name = "Custom Override" } else { override.name = name } override.percentage = percentage override.indefinite = indefinite override.duration = NSDecimalNumber(decimal: duration) if target_override { override.target = target.map { state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0) } } else { override.target = 0 } override.advancedSettings = advancedSettings override.smbIsOff = smbIsOff override.smbIsScheduledOff = smbIsScheduledOff override.start = start.map { NSDecimalNumber(decimal: $0) } override.end = end.map { NSDecimalNumber(decimal: $0) } override.isfAndCr = isfAndCr override.isf = isf override.cr = cr override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) } override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) } override.isUploadedToNS = false } private func resetValues() { name = override.name ?? "" percentage = override.percentage indefinite = override.indefinite duration = override.duration?.decimalValue ?? 0 target = override.target?.decimalValue advancedSettings = override.advancedSettings smbIsOff = override.smbIsOff smbIsScheduledOff = override.smbIsScheduledOff start = override.start?.decimalValue end = override.end?.decimalValue isfAndCr = override.isfAndCr isf = override.isf cr = override.cr smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes } }