import Foundation import SwiftUI struct AddOverrideForm: View { @Environment(\.presentationMode) var presentationMode @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) var dismiss @Environment(AppState.self) var appState @Bindable var state: Adjustments.StateModel @State private var selectedIsfCrOption: IsfAndOrCrOptions = .isfAndCr @State private var selectedDisableSmbOption: DisableSmbOptions = .dontDisable @State private var percentageStep: Int = 5 @State private var displayPickerPercentage: Bool = false @State private var displayPickerDuration: Bool = false @State private var targetStep: Decimal = 5 @State private var displayPickerTarget: Bool = false @State private var displayPickerDisableSmbSchedule: Bool = false @State private var displayPickerSmbMinutes: Bool = false @State private var durationHours = 0 @State private var durationMinutes = 0 @State private var overrideTarget = false @State private var didPressSave = false var body: some View { NavigationView { List { addOverride() saveButton } .listSectionSpacing(10) .padding(.top, 30) .ignoresSafeArea(edges: .top) .scrollContentBackground(.hidden) .background(appState.trioBackgroundColor(for: colorScheme)) .navigationTitle("Add Override") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button(action: { presentationMode.wrappedValue.dismiss() }, label: { Text("Cancel") }) } ToolbarItem(placement: .topBarTrailing) { Button( action: { state.isHelpSheetPresented.toggle() }, label: { Image(systemName: "questionmark.circle") } ) } } .onAppear { targetStep = state.units == .mgdL ? 5 : 9 } .sheet(isPresented: $state.isHelpSheetPresented) { NavigationStack { List { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 0) { Text("This feature can be used to override these therapy settings for a chosen length of time:") .fixedSize(horizontal: false, vertical: true) Text("• Basal Rate") Text(" ◦ Insulin Sensitivity") Text(" ◦ Carb Ratio") Text("• Glucose Target") } Text( "There are also options to override your Max SMB Minutes and Max UAM SMB Minutes, as well as to disable SMBs." ) Text( "Select \"Start Override\" to immediately start using the Override, or select \"Save as Preset\" to be able to easily start the Override at a later time." ) Text( "If an active override preset is edited, the changes will also apply to the currently running override. However, if you edit the currently running override directly, the preset stays unchanged." ) Text( "If using Dynamic ISF (without Sigmoid), overriding your ISF will only adjust the limits of the ISF the algorithm is allowed to set." ) Text( "If using Dynamic ISF (with Sigmoid), overriding your ISF will adjust the ISF used at your glucose target which extends to the ISF used at other glucose. Overriding your glucose target will change glucose level your ISF will be set to your profile ISF. Both of these can be combined in a single Override." ) } } .padding(.trailing, 10) .navigationBarTitle("Help", displayMode: .inline) Button { state.isHelpSheetPresented.toggle() } label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) } .buttonStyle(.bordered) .padding(.top) } .padding() .presentationDetents( [.fraction(0.9), .large], selection: $state.helpSheetDetent ) } } } @ViewBuilder private func addOverride() -> some View { Group { Section { HStack { Text("Name") Spacer() TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing) } } .listRowBackground(Color.chart) Section(footer: state.percentageDescription(state.overridePercentage)) { // Percentage Picker HStack { Text("Change Basal Rate by") Spacer() Text("\(state.overridePercentage.formatted(.number)) %") .foregroundColor(!displayPickerPercentage ? .primary : .accentColor) .onTapGesture { displayPickerPercentage = toggleScrollWheel(displayPickerPercentage) } } if displayPickerPercentage { HStack { // Radio buttons and text on the left side VStack(alignment: .leading) { // Radio buttons for step iteration ForEach([1, 5], id: \.self) { step in RadioButton(isSelected: percentageStep == step, label: "\(step) %") { percentageStep = step state.overridePercentage = Adjustments.StateModel.roundOverridePercentageToStep( state.overridePercentage, step ) } .padding(.top, 10) } } .frame(maxWidth: .infinity) Spacer() // Picker on the right side Picker( selection: Binding( get: { Int(truncating: state.overridePercentage as NSNumber) }, set: { state.overridePercentage = Double($0) } ), label: Text("") ) { ForEach(Array(stride(from: 40, through: 150, by: percentageStep)), id: \.self) { percent in Text("\(percent) %").tag(percent) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } .frame(maxWidth: .infinity) .listRowSeparator(.hidden, edges: .top) } // Picker for ISF/CR settings Picker("Also Inversely Change", 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: state.isfAndCr = true state.isf = true state.cr = true case .isf: state.isfAndCr = false state.isf = true state.cr = false case .cr: state.isfAndCr = false state.isf = false state.cr = true case .nothing: state.isfAndCr = false state.isf = false state.cr = false } } } .listRowBackground(Color.chart) Section { Toggle(isOn: $state.shouldOverrideTarget) { Text("Override Target") } if state.shouldOverrideTarget { let settingsProvider = PickerSettingsProvider.shared let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose) TargetPicker( label: "Target Glucose", selection: Binding( get: { state.target }, set: { state.target = $0 } ), options: settingsProvider.generatePickerValues( from: glucoseSetting, units: state.units, roundMinToStep: true ), units: state.units, targetStep: $targetStep, displayPickerTarget: $displayPickerTarget, toggleScrollWheel: toggleScrollWheel ) .onAppear { if state.target == 0 { state.target = 100 } } } } .listRowBackground(Color.chart) Section { // Picker for ISF/CR 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: state.smbIsOff = false state.smbIsScheduledOff = false case .disable: state.smbIsOff = true state.smbIsScheduledOff = false case .disableOnSchedule: state.smbIsOff = false state.smbIsScheduledOff = true } } if state.smbIsScheduledOff { // First Hour SMBs Are Disabled HStack { Text("From") Spacer() Text( state.is24HourFormat() ? state.format24Hour(Int(truncating: state.start as NSNumber)) + ":00" : state.convertTo12HourFormat(Int(truncating: state.start as NSNumber)) ) .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor) .onTapGesture { displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule) } Spacer() Divider().frame(width: 1, height: 20) Spacer() Text("To") Spacer() Text( state.is24HourFormat() ? state.format24Hour(Int(truncating: state.end as NSNumber)) + ":00" : state.convertTo12HourFormat(Int(truncating: state.end as NSNumber)) ) .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor) .onTapGesture { displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule) } Spacer() } if displayPickerDisableSmbSchedule { HStack { // From Picker Picker(selection: Binding( get: { Int(truncating: state.start as NSNumber) }, set: { state.start = Decimal($0) } ), label: Text("")) { ForEach(0 ..< 24, id: \.self) { hour in Text( state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state .convertTo12HourFormat(hour) ) .tag(hour) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) // To Picker Picker(selection: Binding( get: { Int(truncating: state.end as NSNumber) }, set: { state.end = Decimal($0) } ), label: Text("")) { ForEach(0 ..< 24, id: \.self) { hour in Text( state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state .convertTo12HourFormat(hour) ) .tag(hour) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } .listRowSeparator(.hidden, edges: .top) } } } .listRowBackground(Color.chart) if !state.smbIsOff { Section { Toggle(isOn: $state.advancedSettings) { Text("Override Max SMB Minutes") } if state.advancedSettings { // SMB Minutes Picker HStack { Text("SMB") Spacer() Text("\(state.smbMinutes.formatted(.number)) min") .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor) .onTapGesture { displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes) } Spacer() Divider().frame(width: 1, height: 20) Spacer() Text("UAM") Spacer() Text("\(state.uamMinutes.formatted(.number)) min") .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor) .onTapGesture { displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes) } } if displayPickerSmbMinutes { HStack { Picker(selection: Binding( get: { Int(truncating: state.smbMinutes as NSNumber) }, set: { state.smbMinutes = Decimal($0) } ), label: Text("")) { ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in Text("\(minute) min").tag(minute) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) Picker(selection: Binding( get: { Int(truncating: state.uamMinutes as NSNumber) }, set: { state.uamMinutes = Decimal($0) } ), label: Text("")) { ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in Text("\(minute) min").tag(minute) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) } .listRowSeparator(.hidden, edges: .top) } } } .listRowBackground(Color.chart) } Section { Toggle(isOn: $state.indefinite) { Text("Enable Indefinitely") } if !state.indefinite { HStack { Text("Duration") Spacer() Text(state.formatHrMin(Int(state.overrideDuration))) .foregroundColor(!displayPickerDuration ? .primary : .accentColor) .onTapGesture { displayPickerDuration = toggleScrollWheel(displayPickerDuration) } } if displayPickerDuration { HStack { Picker("Hours", selection: $durationHours) { ForEach(0 ..< 24) { hour in Text("\(hour) hr").tag(hour) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) .onChange(of: durationHours) { state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes) } Picker("Minutes", selection: $durationMinutes) { ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in Text("\(minute) min").tag(minute) } } .pickerStyle(WheelPickerStyle()) .frame(maxWidth: .infinity) .onChange(of: durationMinutes) { state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes) } } .listRowSeparator(.hidden, edges: .top) } } } .listRowBackground(Color.chart) } } private var saveButton: some View { let (isInvalid, errorMessage) = isOverrideInvalid() return Group { Section( header: HStack { Spacer() Text(errorMessage ?? "").textCase(nil) .foregroundColor(colorScheme == .dark ? .orange : .accentColor) Spacer() }, content: { Button(action: { Task { if state.indefinite { state.overrideDuration = 0 } state.isEnabled.toggle() await state.saveCustomOverride() await state.resetStateVariables() dismiss() } }, label: { Text("Start Override") }) .disabled(isInvalid) .frame(maxWidth: .infinity, alignment: .center) .tint(.white) } ).listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue)) Section { Button(action: { Task { await state.saveOverridePreset() dismiss() } }, label: { Text("Save as Preset") }) .disabled(isInvalid) .frame(maxWidth: .infinity, alignment: .center) .tint(.white) } .listRowBackground( isInvalid ? Color(.systemGray4) : Color.secondary ) } } private func toggleScrollWheel(_ toggle: Bool) -> Bool { displayPickerDuration = false displayPickerPercentage = false displayPickerTarget = false displayPickerDisableSmbSchedule = false displayPickerSmbMinutes = false return !toggle } private func isOverrideInvalid() -> (Bool, String?) { let noDurationSpecified = !state.indefinite && state.overrideDuration == 0 let targetZeroWithOverride = state.shouldOverrideTarget && state.target == 0 let allSettingsDefault = state.overridePercentage == 100 && !state.shouldOverrideTarget && !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff if noDurationSpecified { return (true, "Enable indefinitely or set a duration.") } if targetZeroWithOverride { return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).") } if allSettingsDefault { return (true, "All settings are at default values.") } return (false, nil) } }