|
|
@@ -4,11 +4,20 @@ import SwiftUI
|
|
|
struct AddOverrideForm: View {
|
|
|
@Environment(\.presentationMode) var presentationMode
|
|
|
@StateObject var state: OverrideConfig.StateModel
|
|
|
- @State private var isEditing = false
|
|
|
+ @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
|
|
|
@Environment(\.colorScheme) var colorScheme
|
|
|
- @State private var showAlert = false
|
|
|
- @State private var alertString = ""
|
|
|
|
|
|
@Environment(\.dismiss) var dismiss
|
|
|
|
|
|
@@ -20,8 +29,7 @@ struct AddOverrideForm: View {
|
|
|
]),
|
|
|
startPoint: .top,
|
|
|
endPoint: .bottom
|
|
|
- )
|
|
|
- :
|
|
|
+ ) :
|
|
|
LinearGradient(
|
|
|
gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
|
|
|
startPoint: .top,
|
|
|
@@ -36,204 +44,419 @@ struct AddOverrideForm: View {
|
|
|
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
|
|
|
- }
|
|
|
-
|
|
|
- private var alertMessage: String {
|
|
|
- let target: String = state.units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
|
|
|
- return "Please enter a valid target between" + " \(target)."
|
|
|
- }
|
|
|
-
|
|
|
var body: some View {
|
|
|
NavigationView {
|
|
|
- Form {
|
|
|
+ List {
|
|
|
addOverride()
|
|
|
- }.scrollContentBackground(.hidden).background(color)
|
|
|
- .navigationTitle("Add Override")
|
|
|
- .navigationBarTitleDisplayMode(.inline)
|
|
|
- .navigationBarItems(leading: Button("Cancel") {
|
|
|
- presentationMode.wrappedValue.dismiss()
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @ViewBuilder private func addOverride() -> some View {
|
|
|
- Section {
|
|
|
- VStack {
|
|
|
- TextField("Name", text: $state.overrideName)
|
|
|
+ saveButton
|
|
|
}
|
|
|
- } header: {
|
|
|
- Text("Name")
|
|
|
- }.listRowBackground(Color.chart)
|
|
|
-
|
|
|
- Section {
|
|
|
- VStack {
|
|
|
- Spacer()
|
|
|
- Text("\(state.overrideSliderPercentage.formatted(.number)) %")
|
|
|
- .foregroundColor(
|
|
|
- state
|
|
|
- .overrideSliderPercentage >= 130 ? .red :
|
|
|
- (isEditing ? .orange : Color.tabBar)
|
|
|
- )
|
|
|
- .font(.largeTitle)
|
|
|
- Slider(
|
|
|
- value: $state.overrideSliderPercentage,
|
|
|
- in: 10 ... 200,
|
|
|
- step: 1,
|
|
|
- onEditingChanged: { editing in
|
|
|
- isEditing = editing
|
|
|
- }
|
|
|
- )
|
|
|
- Spacer()
|
|
|
- Toggle(isOn: $state.indefinite) {
|
|
|
- Text("Enable indefinitely")
|
|
|
+ .listSectionSpacing(10)
|
|
|
+ .padding(.top, 30)
|
|
|
+ .ignoresSafeArea(edges: .top)
|
|
|
+ .scrollContentBackground(.hidden).background(color)
|
|
|
+ .navigationTitle("Add Override")
|
|
|
+ .navigationBarTitleDisplayMode(.inline)
|
|
|
+ .toolbar {
|
|
|
+ ToolbarItem(placement: .topBarLeading) {
|
|
|
+ Button(action: {
|
|
|
+ presentationMode.wrappedValue.dismiss()
|
|
|
+ }, label: {
|
|
|
+ Text("Cancel")
|
|
|
+ })
|
|
|
}
|
|
|
- }
|
|
|
- if !state.indefinite {
|
|
|
- HStack {
|
|
|
- Text("Duration")
|
|
|
- TextFieldWithToolBar(text: $state.overrideDuration, placeholder: "0", numberFormatter: formatter)
|
|
|
- Text("minutes").foregroundColor(.secondary)
|
|
|
+ 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 {
|
|
|
+ Text("Lorem Ipsum Dolor Sit Amet")
|
|
|
+ }
|
|
|
+ .padding(.trailing, 10)
|
|
|
+ .navigationBarTitle("Help", displayMode: .inline)
|
|
|
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.shouldOverrideTarget) {
|
|
|
- Text("Override Profile Target")
|
|
|
+ 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
|
|
|
+ )
|
|
|
}
|
|
|
- if state.shouldOverrideTarget {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @ViewBuilder private func addOverride() -> some View {
|
|
|
+ Group {
|
|
|
+ Section {
|
|
|
HStack {
|
|
|
- Text("Target Glucose")
|
|
|
- TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
|
|
|
- Text(state.units.rawValue).foregroundColor(.secondary)
|
|
|
+ Text("Name")
|
|
|
+ Spacer()
|
|
|
+ TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing)
|
|
|
}
|
|
|
}
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.advancedSettings) {
|
|
|
- Text("More options")
|
|
|
+ .listRowBackground(Color.chart)
|
|
|
+
|
|
|
+ Section {
|
|
|
+ Toggle(isOn: $state.indefinite) {
|
|
|
+ Text("Enable Indefinitely")
|
|
|
}
|
|
|
- }
|
|
|
- if state.advancedSettings {
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.smbIsOff) {
|
|
|
- Text("Disable SMBs")
|
|
|
+
|
|
|
+ if !state.indefinite {
|
|
|
+ HStack {
|
|
|
+ Text("Duration")
|
|
|
+ Spacer()
|
|
|
+ Text(formatHrMin(Int(state.overrideDuration)))
|
|
|
+ .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
|
|
|
+ }
|
|
|
+ .onTapGesture {
|
|
|
+ displayPickerDuration.toggle()
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 = Decimal(totalDurationInMinutes())
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 = Decimal(totalDurationInMinutes())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .listRowSeparator(.hidden, edges: .top)
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
+ .listRowBackground(Color.chart)
|
|
|
+
|
|
|
+ Section(footer: percentageDescription(state.overridePercentage)) {
|
|
|
+ // Percentage Picker
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.smbIsAlwaysOff) {
|
|
|
- Text("Schedule when SMBs are Off")
|
|
|
- }.disabled(!state.smbIsOff)
|
|
|
+ Text("Change Basal Rate by")
|
|
|
+ Spacer()
|
|
|
+ Text("\(state.overridePercentage.formatted(.number)) %")
|
|
|
+ .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
|
|
|
}
|
|
|
- if state.smbIsAlwaysOff {
|
|
|
+ .onTapGesture {
|
|
|
+ displayPickerPercentage.toggle()
|
|
|
+ }
|
|
|
+
|
|
|
+ if displayPickerPercentage {
|
|
|
HStack {
|
|
|
- Text("First Hour SMBs are Off (24 hours)")
|
|
|
- TextFieldWithToolBar(text: $state.start, placeholder: "0", numberFormatter: formatter)
|
|
|
- Text("hour").foregroundColor(.secondary)
|
|
|
+ // 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 = OverrideConfig.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)
|
|
|
}
|
|
|
- HStack {
|
|
|
- Text("Last Hour SMBs are Off (24 hours)")
|
|
|
- TextFieldWithToolBar(text: $state.end, placeholder: "0", numberFormatter: formatter)
|
|
|
- Text("hour").foregroundColor(.secondary)
|
|
|
+ .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)
|
|
|
}
|
|
|
}
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.isfAndCr) {
|
|
|
- Text("Change ISF and CR")
|
|
|
+ .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
|
|
|
}
|
|
|
}
|
|
|
- if !state.isfAndCr {
|
|
|
+ }
|
|
|
+ .listRowBackground(Color.chart)
|
|
|
+
|
|
|
+ Section {
|
|
|
+ Toggle(isOn: $state.shouldOverrideTarget) {
|
|
|
+ Text("Override Profile Target")
|
|
|
+ }
|
|
|
+
|
|
|
+ if state.shouldOverrideTarget {
|
|
|
HStack {
|
|
|
- Toggle(isOn: $state.isf) {
|
|
|
- Text("Change ISF")
|
|
|
- }
|
|
|
+ Text("Target Glucose")
|
|
|
+ Spacer()
|
|
|
+ Text(
|
|
|
+ (state.units == .mgdL ? state.target.description : state.target.formattedAsMmolL) + " " + state
|
|
|
+ .units.rawValue
|
|
|
+ )
|
|
|
+ .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
|
|
|
}
|
|
|
- HStack {
|
|
|
- Toggle(isOn: $state.cr) {
|
|
|
- Text("Change CR")
|
|
|
+ .onTapGesture {
|
|
|
+ displayPickerTarget.toggle()
|
|
|
+ }
|
|
|
+
|
|
|
+ if displayPickerTarget {
|
|
|
+ HStack {
|
|
|
+ // Radio buttons and text on the left side
|
|
|
+ VStack(alignment: .leading) {
|
|
|
+ // Radio buttons for step iteration
|
|
|
+ let stepChoices: [Decimal] = state.units == .mgdL ? [1, 5] : [1, 9]
|
|
|
+ ForEach(stepChoices, id: \.self) { step in
|
|
|
+ let label = (state.units == .mgdL ? step.description : step.formattedAsMmolL) + " " +
|
|
|
+ state.units.rawValue
|
|
|
+
|
|
|
+ RadioButton(
|
|
|
+ isSelected: targetStep == step,
|
|
|
+ label: label
|
|
|
+ ) {
|
|
|
+ targetStep = step
|
|
|
+ state.target = OverrideConfig.StateModel.roundTargetToStep(state.target, targetStep)
|
|
|
+ }
|
|
|
+ .padding(.top, 10)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
+
|
|
|
+ Spacer()
|
|
|
+
|
|
|
+ // Picker on the right side
|
|
|
+ Picker(selection: Binding(
|
|
|
+ get: { OverrideConfig.StateModel.roundTargetToStep(state.target, targetStep) },
|
|
|
+ set: { state.target = $0 }
|
|
|
+ ), label: Text("")) {
|
|
|
+ ForEach(
|
|
|
+ generateTargetPickerValues(),
|
|
|
+ id: \.self
|
|
|
+ ) { glucose in
|
|
|
+ Text(
|
|
|
+ (state.units == .mgdL ? glucose.description : glucose.formattedAsMmolL) + " " + state
|
|
|
+ .units.rawValue
|
|
|
+ )
|
|
|
+ .tag(glucose)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .pickerStyle(WheelPickerStyle())
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
}
|
|
|
+ .listRowSeparator(.hidden, edges: .top)
|
|
|
}
|
|
|
}
|
|
|
- HStack {
|
|
|
- Text("SMB Minutes")
|
|
|
- TextFieldWithToolBar(text: $state.smbMinutes, placeholder: "0", numberFormatter: formatter)
|
|
|
- Text("minutes").foregroundColor(.secondary)
|
|
|
- }
|
|
|
- HStack {
|
|
|
- Text("UAM SMB Minutes")
|
|
|
- TextFieldWithToolBar(text: $state.uamMinutes, placeholder: "0", numberFormatter: formatter)
|
|
|
- Text("minutes").foregroundColor(.secondary)
|
|
|
- }
|
|
|
}
|
|
|
+ .listRowBackground(Color.chart)
|
|
|
|
|
|
- startAndSaveProfiles
|
|
|
- }
|
|
|
- header: { Text("Add custom Override") }
|
|
|
- footer: {
|
|
|
- Text(
|
|
|
- "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
|
|
|
- )
|
|
|
- }.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
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- private var startAndSaveProfiles: some View {
|
|
|
- HStack {
|
|
|
- Button("Start new Override") {
|
|
|
- if !state.isInputInvalid(target: state.target) {
|
|
|
- showAlert.toggle()
|
|
|
-
|
|
|
- alertString = "\(state.overrideSliderPercentage.formatted(.number)) %, " +
|
|
|
- (
|
|
|
- state.overrideDuration > 0 || !state
|
|
|
- .indefinite ?
|
|
|
- (
|
|
|
- state
|
|
|
- .overrideDuration
|
|
|
- .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
|
|
|
- " min."
|
|
|
- ) :
|
|
|
- NSLocalizedString(" infinite duration.", comment: "")
|
|
|
- ) +
|
|
|
- (
|
|
|
- (state.target == 0 || !state.shouldOverrideTarget) ? "" :
|
|
|
- (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
|
|
|
- )
|
|
|
- +
|
|
|
- (
|
|
|
- state
|
|
|
- .smbIsOff ?
|
|
|
- NSLocalizedString(
|
|
|
- " SMBs are disabled either by schedule or during the entire duration.",
|
|
|
- comment: ""
|
|
|
- ) : ""
|
|
|
+ if state.smbIsScheduledOff {
|
|
|
+ // First Hour SMBs Are Disabled
|
|
|
+ HStack {
|
|
|
+ Text("From")
|
|
|
+ Spacer()
|
|
|
+ Text(
|
|
|
+ is24HourFormat() ? format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
|
|
|
+ convertTo12HourFormat(Int(truncating: state.start as NSNumber))
|
|
|
)
|
|
|
- +
|
|
|
- "\n\n"
|
|
|
- +
|
|
|
- NSLocalizedString(
|
|
|
- "Starting this override will change your profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Override” will start your new Override or edit your current active Override.",
|
|
|
- comment: ""
|
|
|
+ .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
|
|
|
+ Spacer()
|
|
|
+ Divider().frame(width: 1, height: 20)
|
|
|
+ Spacer()
|
|
|
+ Text("To")
|
|
|
+ Spacer()
|
|
|
+ Text(
|
|
|
+ is24HourFormat() ? format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
|
|
|
+ convertTo12HourFormat(Int(truncating: state.end as NSNumber))
|
|
|
)
|
|
|
+ .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
|
|
|
+ Spacer()
|
|
|
+ }
|
|
|
+ .onTapGesture {
|
|
|
+ displayPickerDisableSmbSchedule.toggle()
|
|
|
+ }
|
|
|
+
|
|
|
+ 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(is24HourFormat() ? format24Hour(hour) + ":00" : 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(is24HourFormat() ? format24Hour(hour) + ":00" : 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)
|
|
|
+ Spacer()
|
|
|
+ Divider().frame(width: 1, height: 20)
|
|
|
+ Spacer()
|
|
|
+ Text("UAM")
|
|
|
+ Spacer()
|
|
|
+ Text("\(state.uamMinutes.formatted(.number)) min")
|
|
|
+ .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
|
|
|
+ }
|
|
|
+ .onTapGesture {
|
|
|
+ displayPickerSmbMinutes.toggle()
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
}
|
|
|
- .disabled(unChanged())
|
|
|
- .buttonStyle(BorderlessButtonStyle())
|
|
|
- .font(.callout)
|
|
|
- .controlSize(.mini)
|
|
|
- .alert(
|
|
|
- "Start Override",
|
|
|
- isPresented: $showAlert,
|
|
|
- actions: {
|
|
|
- Button("Cancel", role: .cancel) { state.isEnabled = false }
|
|
|
- Button("Start Override", role: .destructive) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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()
|
|
|
@@ -241,48 +464,182 @@ struct AddOverrideForm: View {
|
|
|
await state.resetStateVariables()
|
|
|
dismiss()
|
|
|
}
|
|
|
- }
|
|
|
- },
|
|
|
- message: {
|
|
|
- Text(alertString)
|
|
|
+ }, label: {
|
|
|
+ Text("Enact Override")
|
|
|
+ })
|
|
|
+ .disabled(isInvalid)
|
|
|
+ .frame(maxWidth: .infinity, alignment: .center)
|
|
|
+ .tint(.white)
|
|
|
}
|
|
|
- )
|
|
|
- .alert(isPresented: $state.showInvalidTargetAlert) {
|
|
|
- Alert(
|
|
|
- title: Text("Invalid Input"),
|
|
|
- message: Text("\(state.alertMessage)"),
|
|
|
- dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
|
|
|
- )
|
|
|
- }
|
|
|
- Button {
|
|
|
- Task {
|
|
|
- if !state.isInputInvalid(target: state.target) {
|
|
|
+ ).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)
|
|
|
}
|
|
|
- label: { Text("Save as Preset") }
|
|
|
- .tint(.orange)
|
|
|
- .frame(maxWidth: .infinity, alignment: .trailing)
|
|
|
- .buttonStyle(BorderlessButtonStyle())
|
|
|
- .controlSize(.mini)
|
|
|
- .disabled(unChanged())
|
|
|
+ .listRowBackground(
|
|
|
+ isInvalid ? Color(.systemGray4) : Color.secondary
|
|
|
+ )
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func unChanged() -> Bool {
|
|
|
- let isChanged = (
|
|
|
- state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && !state
|
|
|
- .advancedSettings
|
|
|
- ) ||
|
|
|
- (!state.indefinite && state.overrideDuration == 0) || (state.shouldOverrideTarget && state.target == 0) ||
|
|
|
- (
|
|
|
- state.overrideSliderPercentage == 100 && !state.shouldOverrideTarget && !state.smbIsOff && state.isf && state
|
|
|
- .cr && state
|
|
|
- .smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
|
|
|
- )
|
|
|
+ private func totalDurationInMinutes() -> Int {
|
|
|
+ let durationTotal = (durationHours * 60) + durationMinutes
|
|
|
+ return max(0, durationTotal)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
|
|
|
- return isChanged
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ func generateTargetPickerValues() -> [Decimal] {
|
|
|
+ var values: [Decimal] = []
|
|
|
+ var currentValue: Double = 72
|
|
|
+ let step = Double(targetStep)
|
|
|
+
|
|
|
+ // Adjust currentValue to be divisible by targetStep
|
|
|
+ let remainder = currentValue.truncatingRemainder(dividingBy: step)
|
|
|
+ if remainder != 0 {
|
|
|
+ // Move currentValue up to the next value divisible by targetStep
|
|
|
+ currentValue += (step - remainder)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Now generate the picker values starting from currentValue
|
|
|
+ while currentValue <= 270 {
|
|
|
+ values.append(Decimal(currentValue))
|
|
|
+ currentValue += step
|
|
|
+ }
|
|
|
+
|
|
|
+ // Glucose values are stored as mg/dl values, so Integers.
|
|
|
+ // Filter out duplicate values when rounded to 1 decimal place.
|
|
|
+ if state.units == .mmolL {
|
|
|
+ // Use a Set to track unique values rounded to 1 decimal
|
|
|
+ var uniqueRoundedValues = Set<String>()
|
|
|
+ values = values.filter { value in
|
|
|
+ let roundedValue = String(format: "%.1f", NSDecimalNumber(decimal: value.asMmolL).doubleValue)
|
|
|
+ return uniqueRoundedValues.insert(roundedValue).inserted
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return values
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+enum IsfAndOrCrOptions: String, CaseIterable {
|
|
|
+ case isfAndCr = "ISF/CR"
|
|
|
+ case isf = "ISF"
|
|
|
+ case cr = "CR"
|
|
|
+ case nothing = "None"
|
|
|
+}
|
|
|
+
|
|
|
+enum DisableSmbOptions: String, CaseIterable {
|
|
|
+ case dontDisable = "Don't Disable"
|
|
|
+ case disable = "Disable"
|
|
|
+ case disableOnSchedule = "Disable on Schedule"
|
|
|
+}
|
|
|
+
|
|
|
+func percentageDescription(_ percent: Double) -> Text? {
|
|
|
+ if percent.isNaN || percent == 100 { return nil }
|
|
|
+
|
|
|
+ var description: String = "Insulin doses will be "
|
|
|
+
|
|
|
+ if percent < 100 {
|
|
|
+ description += "decreased by "
|
|
|
+ } else {
|
|
|
+ description += "increased by "
|
|
|
}
|
|
|
+
|
|
|
+ let deviationFrom100 = abs(percent - 100)
|
|
|
+ description += String(format: "%.0f% %.", deviationFrom100)
|
|
|
+
|
|
|
+ return Text(description)
|
|
|
}
|
|
|
+
|
|
|
+// Function to check if the phone is using 24-hour format
|
|
|
+func is24HourFormat() -> Bool {
|
|
|
+ let formatter = DateFormatter()
|
|
|
+ formatter.locale = Locale.current
|
|
|
+ formatter.dateStyle = .none
|
|
|
+ formatter.timeStyle = .short
|
|
|
+ let dateString = formatter.string(from: Date())
|
|
|
+
|
|
|
+ return !dateString.contains("AM") && !dateString.contains("PM")
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function to convert hours to AM/PM format
|
|
|
+func convertTo12HourFormat(_ hour: Int) -> String {
|
|
|
+ let formatter = DateFormatter()
|
|
|
+ formatter.dateFormat = "h a"
|
|
|
+
|
|
|
+ // Create a date from the hour and format it to AM/PM
|
|
|
+ let calendar = Calendar.current
|
|
|
+ let components = DateComponents(hour: hour)
|
|
|
+ let date = calendar.date(from: components) ?? Date()
|
|
|
+
|
|
|
+ return formatter.string(from: date)
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function to format 24-hour numbers as two digits
|
|
|
+func format24Hour(_ hour: Int) -> String {
|
|
|
+ String(format: "%02d", hour)
|
|
|
+}
|
|
|
+
|
|
|
+//
|
|
|
+// func formatHrMin(_ durationInMinutes: Int) -> String {
|
|
|
+// let hours = durationInMinutes / 60
|
|
|
+// let minutes = durationInMinutes % 60
|
|
|
+//
|
|
|
+// switch (hours, minutes) {
|
|
|
+// case let (0, m):
|
|
|
+// return "\(m) min"
|
|
|
+// case let (h, 0):
|
|
|
+// return "\(h) hr"
|
|
|
+// default:
|
|
|
+// return "\(hours) hr \(minutes) min"
|
|
|
+// }
|
|
|
+// }
|
|
|
+//
|
|
|
+// struct RadioButton: View {
|
|
|
+// var isSelected: Bool
|
|
|
+// var label: String
|
|
|
+// var action: () -> Void
|
|
|
+//
|
|
|
+// var body: some View {
|
|
|
+// Button(action: {
|
|
|
+// action()
|
|
|
+// }) {
|
|
|
+// HStack {
|
|
|
+// Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
|
|
|
+// Text(label) // Add label inside the button to make it tappable
|
|
|
+// }
|
|
|
+// }
|
|
|
+// .buttonStyle(PlainButtonStyle())
|
|
|
+// }
|
|
|
+// }
|