| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 |
- 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
- }
- }
|