| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718 |
- import Foundation
- import SwiftUI
- struct EditOverrideForm: View {
- var override: OverrideStored
- @Environment(\.presentationMode) var presentationMode
- @Environment(\.colorScheme) var colorScheme
- @Bindable 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 percentageStep: Int = 1
- @State private var displayPickerPercentage: Bool = false
- @State private var displayPickerDuration: Bool = false
- @State private var targetStep: Decimal = 1
- @State private var displayPickerTarget: Bool = false
- @State private var displayPickerDisableSmbSchedule: Bool = false
- @State private var displayPickerSmbMinutes: Bool = false
- init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
- override = overrideToEdit
- _state = Bindable(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: overrideToEdit.target?.decimalValue)
- _target_override = State(initialValue: overrideToEdit.target != nil && 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 : .nothing))
- )
- _selectedDisableSmbOption = State(
- initialValue: overrideToEdit.smbIsScheduledOff ? .disableOnSchedule
- : (overrideToEdit.smbIsOff ? .disable : .dontDisable)
- )
- _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
- _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
- }
- 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 percentageSelection: Binding<Double> {
- Binding<Double>(
- get: {
- let value = floor(percentage / Double(percentageStep)) * Double(percentageStep)
- return max(10, min(value, 200))
- },
- set: {
- percentage = $0
- hasChanges = true
- }
- )
- }
- var body: some View {
- NavigationView {
- List {
- editOverride()
- saveButton
- }
- .listSectionSpacing(10)
- .padding(.top, 30)
- .ignoresSafeArea(edges: .top)
- .scrollContentBackground(.hidden).background(color)
- .navigationTitle("Edit 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 }
- .onDisappear {
- if !hasChanges {
- // Reset UI changes
- resetValues()
- }
- }
- .sheet(isPresented: $state.isHelpSheetPresented) {
- NavigationStack {
- List {
- Text("Lorem Ipsum Dolor Sit Amet")
- }
- .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 editOverride() -> some View {
- Group {
- if override.name != nil {
- Section {
- HStack {
- Text("Name")
- Spacer()
- TextField("Name", text: $name)
- .onChange(of: name) { hasChanges = true }
- .multilineTextAlignment(.trailing)
- }
- }
- .listRowBackground(Color.chart)
- }
- Section {
- Toggle(isOn: $indefinite) { Text("Enable Indefinitely") }
- .onChange(of: indefinite) { hasChanges = true }
- if !indefinite {
- HStack {
- Text("Duration")
- Spacer()
- Text(formatHrMin(Int(truncating: duration as NSNumber)))
- .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
- }
- .onTapGesture {
- displayPickerDuration = toggleScrollWheel(displayPickerDuration)
- }
- if displayPickerDuration {
- HStack {
- Picker(
- selection: Binding(
- get: {
- Int(truncating: duration as NSNumber) / 60
- },
- set: {
- let minutes = Int(truncating: duration as NSNumber) % 60
- let totalMinutes = $0 * 60 + minutes
- duration = Decimal(totalMinutes)
- hasChanges = true
- }
- ),
- label: Text("")
- ) {
- ForEach(0 ..< 24) { hour in
- Text("\(hour) hr").tag(hour)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- 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(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- // Percentage Picker
- Section(footer: percentageDescription(percentage)) {
- HStack {
- Text("Change Basal Rate by")
- Spacer()
- Text("\(percentage.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
- percentage = OverrideConfig.StateModel.roundOverridePercentageToStep(percentage, step)
- }
- .padding(.top, 10)
- }
- }
- .frame(maxWidth: .infinity)
- Spacer()
- // Picker on the right side
- Picker(
- selection: percentageSelection,
- label: Text("")
- ) {
- ForEach(
- Array(stride(from: 40.0, through: 150.0, by: Double(percentageStep))),
- id: \.self
- ) { percent in
- Text("\(Int(percent)) %").tag(percent)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- // Picker for ISF/CR settings
- Picker("Also 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:
- isfAndCr = true
- isf = false
- cr = false
- case .isf:
- isfAndCr = false
- isf = true
- cr = false
- case .cr:
- isfAndCr = false
- isf = false
- cr = true
- case .nothing:
- isfAndCr = false
- isf = false
- cr = false
- }
- hasChanges = true
- }
- }
- .listRowBackground(Color.chart)
- Section {
- Toggle(isOn: $target_override) {
- Text("Override Target")
- }
- .onChange(of: target_override) {
- hasChanges = true
- }
- // Target Glucose Picker
- if target_override {
- let settingsProvider = PickerSettingsProvider.shared
- let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose)
- TargetPicker(
- label: "Target Glucose",
- selection: Binding(
- get: { target ?? 100 },
- set: { target = $0 }
- ),
- options: settingsProvider.generatePickerValues(
- from: glucoseSetting,
- units: state.units,
- roundMinToStep: true
- ),
- units: state.units,
- hasChanges: $hasChanges,
- targetStep: $targetStep,
- displayPickerTarget: $displayPickerTarget,
- toggleScrollWheel: toggleScrollWheel
- )
- .onAppear {
- if target == 0 || target == nil {
- target = 100
- }
- }
- }
- }
- .listRowBackground(Color.chart)
- Section {
- // 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
- HStack {
- Text("From")
- Spacer()
- Text(
- is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
- convertTo12HourFormat(Int(truncating: start! as NSNumber))
- )
- .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
- Spacer()
- Divider().frame(width: 1, height: 20)
- Spacer()
- Text("To")
- Spacer()
- Text(
- is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
- convertTo12HourFormat(Int(truncating: end! as NSNumber))
- )
- .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
- }
- .onTapGesture {
- displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
- }
- if displayPickerDisableSmbSchedule {
- HStack {
- 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)
- 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)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- if !smbIsOff {
- Section {
- Toggle(isOn: $advancedSettings) {
- Text("Change Max SMB Minutes")
- }
- .onChange(of: advancedSettings) { hasChanges = true }
- if advancedSettings {
- // SMB Minutes Picker
- HStack {
- Text("SMB")
- Spacer()
- Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
- .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
- Spacer()
- Divider().frame(width: 1, height: 20)
- Spacer()
- Text("UAM")
- Spacer()
- Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
- .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
- }
- .onTapGesture {
- displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
- }
- if displayPickerSmbMinutes {
- HStack {
- 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)
- 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)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- .listRowBackground(Color.chart)
- }
- }
- }
- private var saveButton: some View {
- let (isInvalid, errorMessage) = isOverrideInvalid()
- return Section(
- header:
- HStack {
- Spacer()
- Text(errorMessage ?? "").textCase(nil)
- .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
- Spacer()
- },
- content: {
- Button(action: {
- saveChanges()
- do {
- guard let moc = override.managedObjectContext else { return }
- guard moc.hasChanges else { return }
- try moc.save()
- Task {
- await state.nightscoutManager.uploadProfiles()
- }
- // Disable previous active Override
- 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 Override")
- })
- .disabled(isInvalid) // Disable button if changes are invalid
- .frame(maxWidth: .infinity, alignment: .center)
- .tint(.white)
- }
- )
- .listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
- }
- private func isOverrideInvalid() -> (Bool, String?) {
- let noDurationSpecified = !indefinite && duration == 0
- let targetZeroWithOverride = target_override && (target ?? 0 < 72 || target ?? 0 > 270)
- let allSettingsDefault = percentage == 100 && !target_override && !advancedSettings &&
- !smbIsOff && !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.")
- }
- if !hasChanges {
- return (true, nil)
- }
- return (false, nil)
- }
- 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)
- override.target = target_override ? NSDecimalNumber(decimal: target ?? 100) : nil
- 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
- }
- private func toggleScrollWheel(_ toggle: Bool) -> Bool {
- displayPickerDuration = false
- displayPickerPercentage = false
- displayPickerTarget = false
- displayPickerDisableSmbSchedule = false
- displayPickerSmbMinutes = false
- return !toggle
- }
- }
- struct TargetPicker: View {
- let label: String
- @Binding var selection: Decimal
- let options: [Decimal]
- let units: GlucoseUnits
- @Binding var hasChanges: Bool
- @Binding var targetStep: Decimal
- @Binding var displayPickerTarget: Bool
- var toggleScrollWheel: (_ picker: Bool) -> Bool
- var body: some View {
- VStack {
- HStack {
- Text(label)
- Spacer()
- Text(
- (units == .mgdL ? selection.description : selection.formattedAsMmolL) + " " + units.rawValue
- )
- .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
- }
- .onTapGesture {
- displayPickerTarget = toggleScrollWheel(displayPickerTarget)
- }
- if displayPickerTarget {
- HStack {
- // Radio buttons and text on the left side
- VStack(alignment: .leading) {
- // Radio buttons for step iteration
- let stepChoices: [Decimal] = units == .mgdL ? [1, 5] : [1, 9]
- ForEach(stepChoices, id: \.self) { step in
- let label = (units == .mgdL ? step.description : step.formattedAsMmolL) + " " +
- units.rawValue
- RadioButton(
- isSelected: targetStep == step,
- label: label
- ) {
- targetStep = step
- selection = OverrideConfig.StateModel.roundTargetToStep(selection, step)
- }
- .padding(.top, 10)
- }
- }
- .frame(maxWidth: .infinity)
- Spacer()
- // Picker on the right side
- Picker(selection: Binding(
- get: { OverrideConfig.StateModel.roundTargetToStep(selection, targetStep) },
- set: {
- selection = $0
- hasChanges = true
- }
- ), label: Text("")) {
- ForEach(options, id: \.self) { option in
- Text((units == .mgdL ? option.description : option.formattedAsMmolL) + " " + units.rawValue)
- .tag(option)
- }
- }
- .pickerStyle(WheelPickerStyle())
- .frame(maxWidth: .infinity)
- }
- .listRowSeparator(.hidden, edges: .top)
- }
- }
- }
- }
|