Sfoglia il codice sorgente

Merge branch 'override-update' of github.com:MikePlante1/Trio-dev into tempTargets

Deniz Cengiz 1 anno fa
parent
commit
83136071d5

+ 5 - 1
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -411,7 +411,8 @@ final class OpenAPS {
             // Fetch data for the past two hours
             let twoHoursArray = uniqueEvents.filter { ($0["timestamp"] as? Date ?? Date()) >= twoHoursAgo }
             var nrOfIndices = twoHoursArray.count
-            let totalAmount = twoHoursArray.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }.reduce(0, +)
+            let totalAmount = twoHoursArray.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }
+                .reduce(0, +)
 
             var useOverride = overrideArray.first?.enabled ?? false
             var overridePercentage = Decimal(overrideArray.first?.percentage ?? 100)
@@ -502,6 +503,9 @@ final class OpenAPS {
                 isfAndCr: overrideArray.first?.isfAndCr ?? false,
                 isf: overrideArray.first?.isf ?? false,
                 cr: overrideArray.first?.cr ?? false,
+                smbIsScheduledOff: overrideArray.first?.smbIsScheduledOff ?? false,
+                start: (overrideArray.first?.start ?? 0) as Decimal,
+                end: (overrideArray.first?.end ?? 0) as Decimal,
                 smbMinutes: overrideArray.first?.smbMinutes?.decimalValue ?? smbMinutes,
                 uamMinutes: overrideArray.first?.uamMinutes?.decimalValue ?? uamMinutes
             )

+ 13 - 20
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -124,38 +124,31 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             newOverride.duration = override.duration as NSDecimalNumber
             newOverride.indefinite = override.indefinite
             newOverride.percentage = override.percentage
+            newOverride.isfAndCr = override.isfAndCr
+            newOverride.isf = override.isf
+            newOverride.cr = override.cr
             newOverride.enabled = override.enabled
             newOverride.smbIsOff = override.smbIsOff
             if override.overrideTarget {
-                newOverride.target = (
-                    self.settingsManager.settings.units == .mmolL ? override.target.asMgdL : override.target
-                ) as NSDecimalNumber
+                newOverride.target = override.target as NSDecimalNumber
             } else {
                 newOverride.target = 0
             }
             if override.advancedSettings {
                 newOverride.advancedSettings = true
 
-                if !override.isfAndCr {
-                    newOverride.isfAndCr = false
-                    newOverride.isf = override.isf
-                    newOverride.cr = override.cr
-                } else {
-                    newOverride.isfAndCr = true
-                }
-
-                if override.smbIsAlwaysOff {
-                    newOverride.smbIsAlwaysOff = true
-                    newOverride.start = override.start as NSDecimalNumber
-                    newOverride.end = override.end as NSDecimalNumber
-                } else {
-                    newOverride.smbIsAlwaysOff = false
-                }
-
                 newOverride.smbMinutes = override.smbMinutes as NSDecimalNumber
                 newOverride.uamMinutes = override.uamMinutes as NSDecimalNumber
             }
 
+            if override.smbIsScheduledOff {
+                newOverride.smbIsScheduledOff = true
+                newOverride.start = override.start as NSDecimalNumber
+                newOverride.end = override.end as NSDecimalNumber
+            } else {
+                newOverride.smbIsScheduledOff = false
+            }
+
             do {
                 guard self.backgroundContext.hasChanges else { return }
                 try self.backgroundContext.save()
@@ -184,7 +177,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         newOverride.isfAndCr = override.isfAndCr
         newOverride.isf = override.isf
         newOverride.cr = override.cr
-        newOverride.smbIsAlwaysOff = override.smbIsAlwaysOff
+        newOverride.smbIsScheduledOff = override.smbIsScheduledOff
         newOverride.start = override.start
         newOverride.end = override.end
         newOverride.smbMinutes = override.smbMinutes

+ 12 - 0
FreeAPS/Sources/Models/Oref2_variables.swift

@@ -18,6 +18,9 @@ struct Oref2_variables: JSON, Equatable {
     var isfAndCr: Bool
     var isf: Bool
     var cr: Bool
+    var smbIsScheduledOff: Bool
+    var start: Decimal
+    var end: Decimal
     var smbMinutes: Decimal
     var uamMinutes: Decimal
 
@@ -39,6 +42,9 @@ struct Oref2_variables: JSON, Equatable {
         isfAndCr: Bool,
         isf: Bool,
         cr: Bool,
+        smbIsScheduledOff: Bool,
+        start: Decimal,
+        end: Decimal,
         smbMinutes: Decimal,
         uamMinutes: Decimal
     ) {
@@ -59,6 +65,9 @@ struct Oref2_variables: JSON, Equatable {
         self.isfAndCr = isfAndCr
         self.isf = isf
         self.cr = cr
+        self.smbIsScheduledOff = smbIsScheduledOff
+        self.start = start
+        self.end = end
         self.smbMinutes = smbMinutes
         self.uamMinutes = uamMinutes
     }
@@ -83,6 +92,9 @@ extension Oref2_variables {
         case isfAndCr
         case isf
         case cr
+        case smbIsScheduledOff
+        case start
+        case end
         case smbMinutes
         case uamMinutes
     }

+ 1 - 1
FreeAPS/Sources/Models/Override.swift

@@ -16,7 +16,7 @@ struct Override {
     let isfAndCr: Bool
     let isf: Bool
     let cr: Bool
-    let smbIsAlwaysOff: Bool
+    let smbIsScheduledOff: Bool
     let start: Decimal
     let end: Decimal
     let smbMinutes: Decimal

+ 15 - 15
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -135,6 +135,21 @@ extension MainChartView {
                 drawEndRuleMark()
                 drawCurrentTimeMarker()
 
+                OverrideView(
+                    state: state,
+                    overrides: state.overrides,
+                    overrideRunStored: state.overrideRunStored,
+                    units: state.units,
+                    viewContext: context
+                )
+
+                TempTargetView(
+                    tempTargetStored: state.tempTargetStored,
+                    tempTargetRunStored: state.tempTargetRunStored,
+                    units: state.units,
+                    viewContext: context
+                )
+
                 GlucoseChartView(
                     glucoseData: state.glucoseFromPersistence,
                     units: state.units,
@@ -159,21 +174,6 @@ extension MainChartView {
                     minValue: state.minYAxisValue
                 )
 
-                OverrideView(
-                    state: state,
-                    overrides: state.overrides,
-                    overrideRunStored: state.overrideRunStored,
-                    units: state.units,
-                    viewContext: context
-                )
-
-                TempTargetView(
-                    tempTargetStored: state.tempTargetStored,
-                    tempTargetRunStored: state.tempTargetRunStored,
-                    units: state.units,
-                    viewContext: context
-                )
-
                 ForecastView(
                     preprocessedData: state.preprocessedData,
                     minForecast: state.minForecast,

+ 42 - 7
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -214,11 +214,10 @@ extension Home {
 
             if !indefinite {
                 if newDuration >= 1 {
-                    durationString =
-                        "\(newDuration.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) min"
+                    durationString = formatHrMin(Int(newDuration))
                 } else if newDuration > 0 {
-                    durationString =
-                        "\((newDuration * 60).formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) s"
+                    durationString = "\(Int(newDuration * 60)) s"
+
                 } else {
                     /// Do not show the Override anymore
                     Task {
@@ -228,9 +227,15 @@ extension Home {
                 }
             }
 
-            let smbToggleString = latestOverride.smbIsOff ? " \u{20e0}" : ""
+            let smbScheduleString = latestOverride
+                .smbIsScheduledOff && ((latestOverride.start?.stringValue ?? "") != (latestOverride.end?.stringValue ?? ""))
+                ? " \(formatTimeRange(start: latestOverride.start?.stringValue, end: latestOverride.end?.stringValue))"
+                : ""
+
+            let smbToggleString = latestOverride.smbIsOff || latestOverride
+                .smbIsScheduledOff ? "SMBs Off\(smbScheduleString)" : ""
 
-            let components = [percentString, targetString, durationString, smbToggleString].filter { !$0.isEmpty }
+            let components = [durationString, percentString, targetString, smbToggleString].filter { !$0.isEmpty }
             return components.isEmpty ? nil : components.joined(separator: ", ")
         }
 
@@ -681,7 +686,6 @@ extension Home {
                     } message: {
                         Text("Select Adjustment")
                     }
-
             }.padding(.horizontal, 10).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 10))
         }
 
@@ -1114,3 +1118,34 @@ extension UIScreen {
         UIScreen.main.bounds.width
     }
 }
+
+// Helper function to convert a start and end hour to either 24-hour or AM/PM format
+func formatTimeRange(start: String?, end: String?) -> String {
+    guard let start = start, let end = end else {
+        return ""
+    }
+
+    // Check if the format is 24-hour or AM/PM
+    if is24HourFormat() {
+        // Return the original 24-hour format
+        return "\(start)-\(end)"
+    } else {
+        // Convert to AM/PM format using DateFormatter
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH"
+
+        if let startHour = Int(start), let endHour = Int(end) {
+            let startDate = Calendar.current.date(bySettingHour: startHour, minute: 0, second: 0, of: Date()) ?? Date()
+            let endDate = Calendar.current.date(bySettingHour: endHour, minute: 0, second: 0, of: Date()) ?? Date()
+
+            // Customize the format to "2p" or "2a"
+            formatter.dateFormat = "ha"
+            let startFormatted = formatter.string(from: startDate).lowercased().replacingOccurrences(of: "m", with: "")
+            let endFormatted = formatter.string(from: endDate).lowercased().replacingOccurrences(of: "m", with: "")
+
+            return "\(startFormatted)-\(endFormatted)"
+        } else {
+            return ""
+        }
+    }
+}

+ 57 - 11
FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift

@@ -10,7 +10,7 @@ extension OverrideConfig {
         @ObservationIgnored @Injected() var apsManager: APSManager!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
 
-        var overrideSliderPercentage: Double = 100
+        var overridePercentage: Double = 100
         var isEnabled = false
         var indefinite = true
         var overrideDuration: Decimal = 0
@@ -26,9 +26,9 @@ extension OverrideConfig {
         var isfAndCr: Bool = true
         var isf: Bool = true
         var cr: Bool = true
-        var smbIsAlwaysOff: Bool = false
+        var smbIsScheduledOff: Bool = false
         var start: Decimal = 0
-        var end: Decimal = 23
+        var end: Decimal = 0
         var smbMinutes: Decimal = 0
         var uamMinutes: Decimal = 0
         var defaultSmbMinutes: Decimal = 0
@@ -77,6 +77,9 @@ extension OverrideConfig {
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
+        var isHelpSheetPresented: Bool = false
+        var helpSheetDetent = PresentationDetent.large
+
         var alertMessage: String {
             let target: String = units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
             return "Please enter a valid target between" + " \(target)."
@@ -360,7 +363,7 @@ extension OverrideConfig.StateModel {
             date: Date(),
             duration: overrideDuration,
             indefinite: indefinite,
-            percentage: overrideSliderPercentage,
+            percentage: overridePercentage,
             smbIsOff: smbIsOff,
             isPreset: isPreset,
             id: id,
@@ -370,7 +373,7 @@ extension OverrideConfig.StateModel {
             isfAndCr: isfAndCr,
             isf: isf,
             cr: cr,
-            smbIsAlwaysOff: smbIsAlwaysOff,
+            smbIsScheduledOff: smbIsScheduledOff,
             start: start,
             end: end,
             smbMinutes: smbMinutes,
@@ -399,7 +402,7 @@ extension OverrideConfig.StateModel {
             date: Date(),
             duration: overrideDuration,
             indefinite: indefinite,
-            percentage: overrideSliderPercentage,
+            percentage: overridePercentage,
             smbIsOff: smbIsOff,
             isPreset: true,
             id: id,
@@ -409,7 +412,7 @@ extension OverrideConfig.StateModel {
             isfAndCr: isfAndCr,
             isf: isf,
             cr: cr,
-            smbIsAlwaysOff: smbIsAlwaysOff,
+            smbIsScheduledOff: smbIsScheduledOff,
             start: start,
             end: end,
             smbMinutes: smbMinutes,
@@ -547,8 +550,7 @@ extension OverrideConfig.StateModel {
 
         overrideDuration = 0
         indefinite = true
-        overrideSliderPercentage = 100
-
+        overridePercentage = 100
         advancedSettings = false
         smbIsOff = false
         overrideName = ""
@@ -556,13 +558,57 @@ extension OverrideConfig.StateModel {
         isf = true
         cr = true
         isfAndCr = true
-        smbIsAlwaysOff = false
+        smbIsScheduledOff = false
         start = 0
-        end = 23
+        end = 0
         smbMinutes = defaultSmbMinutes
         uamMinutes = defaultUamMinutes
         target = currentGlucoseTarget
     }
+
+    static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
+        // Convert target and step to NSDecimalNumber
+        guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
+              let stepValue = NSDecimalNumber(decimal: step).doubleValue as Double?
+        else {
+            return target
+        }
+
+        // Perform the remainder check using truncatingRemainder
+        let remainder = Decimal(targetValue.truncatingRemainder(dividingBy: stepValue))
+
+        if remainder != 0 {
+            // Calculate how much to adjust (up or down) based on the remainder
+            let adjustment = step - remainder
+            return target + adjustment
+        }
+
+        // Return the original target if no adjustment is needed
+        return target
+    }
+
+    static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
+        let stepDouble = Double(step)
+        // Check if overridePercentage is not divisible by the selected step
+        if percentage.truncatingRemainder(dividingBy: stepDouble) != 0 {
+            let roundedValue: Double
+
+            if percentage > 100 {
+                // Round down to the nearest valid step away from 100
+                let stepCount = (percentage - 100) / stepDouble
+                roundedValue = 100 + floor(stepCount) * stepDouble
+            } else {
+                // Round up to the nearest valid step away from 100
+                let stepCount = (100 - percentage) / stepDouble
+                roundedValue = 100 - floor(stepCount) * stepDouble
+            }
+
+            // Ensure the value stays between 10 and 200
+            return max(10, min(roundedValue, 200))
+        }
+
+        return percentage
+    }
 }
 
 // MARK: - Temp Targets

+ 558 - 201
FreeAPS/Sources/Modules/OverrideConfig/View/AddOverrideForm.swift

@@ -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())
+//    }
+// }

+ 545 - 188
FreeAPS/Sources/Modules/OverrideConfig/View/EditOverrideForm.swift

@@ -14,7 +14,7 @@ struct EditOverrideForm: View {
     @State private var target: Decimal?
     @State private var advancedSettings: Bool
     @State private var smbIsOff: Bool
-    @State private var smbIsAlwaysOff: Bool
+    @State private var smbIsScheduledOff: Bool
     @State private var start: Decimal?
     @State private var end: Decimal?
     @State private var isfAndCr: Bool
@@ -22,11 +22,19 @@ struct EditOverrideForm: View {
     @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 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
 
     init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
         override = overrideToEdit
@@ -35,19 +43,24 @@ struct EditOverrideForm: View {
         _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 = State(initialValue: overrideToEdit.target?.decimalValue)
         _target_override = State(initialValue: overrideToEdit.target?.decimalValue != 0)
         _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
         _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
-        _smbIsAlwaysOff = State(initialValue: overrideToEdit.smbIsAlwaysOff)
+        _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)
     }
@@ -75,219 +88,448 @@ struct EditOverrideForm: 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 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 {
-            Form {
+            List {
                 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()
-                    }
+            }
+            .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")
+                    })
                 }
-                .alert(isPresented: $state.showInvalidTargetAlert) {
-                    Alert(
-                        title: Text("Invalid Input"),
-                        message: Text("\(state.alertMessage)"),
-                        dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
+                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 {
-        if override.name != nil {
+        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 {
-                VStack {
-                    TextField("Name", text: $name)
-                        .onChange(of: name) { _ in hasChanges = true }
+                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.toggle()
+                    }
+
+                    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)
+                    }
                 }
-            } header: {
-                Text("Name")
-            }.listRowBackground(Color.chart)
-        }
-        Section {
-            VStack {
-                Spacer()
-                Text("\(percentage.formatted(.number)) %")
-                    .foregroundColor(
-                        state
-                            .overrideSliderPercentage >= 130 ? .red :
-                            (isEditing ? .orange : Color.tabBar)
-                    )
-                    .font(.largeTitle)
-                Slider(
-                    value: $percentage,
-                    in: 10 ... 200,
-                    step: 1
-                ).onChange(of: percentage) { _ in hasChanges = true }
-                Spacer()
-                Toggle(isOn: $indefinite) {
-                    Text("Enable indefinitely")
-                }.onChange(of: indefinite) { _ in hasChanges = true }
             }
-            if !indefinite {
+            .listRowBackground(Color.chart)
+
+            // Percentage Picker
+            Section(footer: percentageDescription(percentage)) {
                 HStack {
-                    Text("Duration")
-                    TextFieldWithToolBar(
-                        text: Binding(
-                            get: { duration },
-                            set: {
-                                duration = $0
-                                hasChanges = true
+                    Text("Change Basal Rate by")
+                    Spacer()
+                    Text("\(percentage.formatted(.number)) %")
+                        .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
+                }
+                .onTapGesture {
+                    displayPickerPercentage.toggle()
+                }
+
+                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)
                             }
-                        ),
-                        placeholder: "0",
-                        numberFormatter: formatter
-                    )
-                    Text("minutes").foregroundColor(.secondary)
+                        }
+                        .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)
 
-            HStack {
+            Section {
                 Toggle(isOn: $target_override) {
-                    Text("Override Override Target")
-                }.onChange(of: target_override) { _ in
+                    Text("Override Target")
+                }
+                .onChange(of: target_override) {
                     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)
+                // Target Glucose Picker
+                if target_override {
+                    TargetPicker(
+                        label: "Target Glucose",
+                        selection: Binding(
+                            get: { target ?? 100 },
+                            set: { target = $0 }
+                        ),
+                        options: generateTargetPickerValues(),
+                        units: state.units,
+                        hasChanges: $hasChanges,
+                        targetStep: $targetStep
+                    )
                 }
             }
+            .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
+                }
 
-            Toggle(isOn: $advancedSettings) {
-                Text("More options")
-            }.onChange(of: advancedSettings) { _ in 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)
 
-            if advancedSettings {
-                Toggle(isOn: $smbIsOff) {
-                    Text("Disable SMBs")
-                }.onChange(of: smbIsOff) { _ in hasChanges = true }
+                        Spacer()
 
-                Toggle(isOn: $smbIsAlwaysOff) {
-                    Text("Schedule when SMBs are Off")
-                }.onChange(of: smbIsAlwaysOff) { _ in hasChanges = true }
+                        Divider().frame(width: 1, height: 20)
 
-                if smbIsAlwaysOff {
-                    HStack {
-                        Text("First Hour SMBs are Off (24 hours)")
-                        TextFieldWithToolBar(
-                            text: Binding(
-                                get: { start ?? 0 },
+                        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.toggle()
+                    }
+
+                    if displayPickerDisableSmbSchedule {
+                        HStack {
+                            Picker(selection: Binding(
+                                get: { Int(truncating: start! as NSNumber) },
                                 set: {
-                                    start = $0
+                                    start = Decimal($0)
                                     hasChanges = true
                                 }
-                            ),
-                            placeholder: "0",
-                            numberFormatter: formatter
-                        )
-                        Text("hour").foregroundColor(.secondary)
-                    }
+                            ), 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)
 
-                    HStack {
-                        Text("Last Hour SMBs are Off (24 hours)")
-                        TextFieldWithToolBar(
-                            text: Binding(
-                                get: { end ?? 23 },
+                            Picker(selection: Binding(
+                                get: { Int(truncating: end! as NSNumber) },
                                 set: {
-                                    end = $0
+                                    end = Decimal($0)
                                     hasChanges = true
                                 }
-                            ),
-                            placeholder: "0",
-                            numberFormatter: formatter
-                        )
-                        Text("hour").foregroundColor(.secondary)
+                            ), 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)
 
-                Toggle(isOn: $isfAndCr) {
-                    Text("Change ISF and CR")
-                }.onChange(of: isfAndCr) { _ in hasChanges = true }
+            if !smbIsOff {
+                Section {
+                    Toggle(isOn: $advancedSettings) {
+                        Text("Change Max SMB Minutes")
+                    }
+                    .onChange(of: advancedSettings) { hasChanges = true }
 
-                if !isfAndCr {
-                    Toggle(isOn: $isf) {
-                        Text("Change ISF")
-                    }.onChange(of: isf) { _ in hasChanges = true }
+                    if advancedSettings {
+                        // SMB Minutes Picker
+                        HStack {
+                            Text("SMB")
+                            Spacer()
+                            Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
+                                .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
 
-                    Toggle(isOn: $cr) {
-                        Text("Change CR")
-                    }.onChange(of: cr) { _ in hasChanges = true }
-                }
+                            Spacer()
 
-                HStack {
-                    Text("SMB Minutes")
-                    TextFieldWithToolBar(
-                        text: Binding(
-                            get: { smbMinutes ?? state.defaultSmbMinutes },
-                            set: {
-                                smbMinutes = $0
-                                hasChanges = true
-                            }
-                        ),
-                        placeholder: "0",
-                        numberFormatter: formatter
-                    )
-                    Text("minutes").foregroundColor(.secondary)
-                }
+                            Divider().frame(width: 1, height: 20)
 
-                HStack {
-                    Text("UAM SMB Minutes")
-                    TextFieldWithToolBar(
-                        text: Binding(
-                            get: { uamMinutes ?? state.defaultUamMinutes },
-                            set: {
-                                uamMinutes = $0
-                                hasChanges = true
+                            Spacer()
+
+                            Text("UAM")
+                            Spacer()
+                            Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
+                                .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+                        }
+                        .onTapGesture {
+                            displayPickerSmbMinutes.toggle()
+                        }
+
+                        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)
                             }
-                        ),
-                        placeholder: "0",
-                        numberFormatter: formatter
-                    )
-                    Text("minutes").foregroundColor(.secondary)
+                            .listRowSeparator(.hidden, edges: .top)
+                        }
+                    }
                 }
+                .listRowBackground(Color.chart)
             }
-        }.listRowBackground(Color.chart)
+        }
     }
 
     private var saveButton: some View {
-        HStack {
-            Spacer()
-            Button(action: {
-                if !state.isInputInvalid(target: target ?? 0) {
+        let (isInvalid, errorMessage) = isOverrideInvalid()
+
+        return Section(
+            header:
+            HStack {
+                Spacer()
+                Text(errorMessage ?? "").textCase(nil)
+                    .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                Spacer()
+            },
+            content: {
+                Button(action: {
                     saveChanges()
 
                     do {
@@ -312,16 +554,40 @@ struct EditOverrideForm: View {
                     } catch {
                         debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
                     }
-                }
-            }, label: {
-                Text("Save")
-            })
-                .disabled(!hasChanges)
-                .frame(maxWidth: .infinity, alignment: .center)
-                .tint(.white)
+                }, 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))
+    }
 
-            Spacer()
-        }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
+    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() {
@@ -333,16 +599,10 @@ struct EditOverrideForm: View {
         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.target = NSDecimalNumber(decimal: target ?? 100)
         override.advancedSettings = advancedSettings
         override.smbIsOff = smbIsOff
-        override.smbIsAlwaysOff = smbIsAlwaysOff
+        override.smbIsScheduledOff = smbIsScheduledOff
         override.start = start.map { NSDecimalNumber(decimal: $0) }
         override.end = end.map { NSDecimalNumber(decimal: $0) }
         override.isfAndCr = isfAndCr
@@ -361,7 +621,7 @@ struct EditOverrideForm: View {
         target = override.target?.decimalValue
         advancedSettings = override.advancedSettings
         smbIsOff = override.smbIsOff
-        smbIsAlwaysOff = override.smbIsAlwaysOff
+        smbIsScheduledOff = override.smbIsScheduledOff
         start = override.start?.decimalValue
         end = override.end?.decimalValue
         isfAndCr = override.isfAndCr
@@ -370,4 +630,101 @@ struct EditOverrideForm: View {
         smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
         uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
     }
+
+    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
+    }
+}
+
+struct TargetPicker: View {
+    let label: String
+    @Binding var selection: Decimal
+    let options: [Decimal]
+    let units: GlucoseUnits
+    @Binding var hasChanges: Bool
+    @Binding var targetStep: Decimal
+    @State private var isDisplayed: Bool = false
+
+    var body: some View {
+        HStack {
+            Text(label)
+            Spacer()
+            Text(
+                (units == .mgdL ? selection.description : selection.formattedAsMmolL) + " " + units.rawValue
+            )
+            .foregroundColor(!isDisplayed ? .primary : .accentColor)
+        }
+        .onTapGesture {
+            isDisplayed.toggle()
+        }
+        if isDisplayed {
+            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)
+        }
+    }
 }

+ 95 - 29
FreeAPS/Sources/Modules/OverrideConfig/View/OverrideRootView.swift

@@ -19,6 +19,8 @@ extension OverrideConfig {
         @State private var selectedTempTarget: TempTargetStored?
 
         // temp targets
+        @State private var isConfirmDeleteShown = false
+        @State private var isPromptPresented = false
         @State private var isRemoveAlertPresented = false
         @State private var removeAlert: Alert?
         @State private var isEditingTT = false
@@ -246,9 +248,8 @@ extension OverrideConfig {
                     overridesView(for: preset)
                         .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                             Button(role: .none) {
-                                Task {
-                                    await state.invokeOverridePresetDeletion(preset.objectID)
-                                }
+                                selectedOverride = preset
+                                isConfirmDeleteShown = true
                             } label: {
                                 Label("Delete", systemImage: "trash")
                                     .tint(.red)
@@ -264,6 +265,44 @@ extension OverrideConfig {
                         }
                 }
                 .onMove(perform: state.reorderOverride)
+                .confirmationDialog(
+                    "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
+                    isPresented: $isConfirmDeleteShown,
+                    titleVisibility: .visible
+                ) {
+                    if let itemToDelete = selectedOverride {
+                        Button(
+                            state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
+                            role: .destructive
+                        ) {
+                            if state.currentActiveOverride == selectedOverride {
+                                Task {
+                                    // Save cancelled Override in OverrideRunStored Entity
+                                    // Cancel ALL active Override
+                                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                                }
+                            }
+                            // Perform the delete action
+                            Task {
+                                await state.invokeOverridePresetDeletion(itemToDelete.objectID)
+                            }
+                            // Reset the selected item after deletion
+                            selectedOverride = nil
+                        }
+                    }
+                    Button("Cancel", role: .cancel) {
+                        // Dismiss the dialog without action
+                        selectedOverride = nil
+                    }
+                } message: {
+                    if state.currentActiveOverride == selectedOverride {
+                        Text(
+                            state
+                                .currentActiveOverride == selectedOverride ?
+                                "This override preset is currently running. Deleting will stop it." : ""
+                        )
+                    }
+                }
                 .listRowBackground(Color.chart)
             } header: {
                 Text("Presets")
@@ -471,27 +510,61 @@ extension OverrideConfig {
             })
         }
 
+        private var overrideLabelDivider: some View {
+            Divider()
+                .frame(width: 1, height: 20)
+        }
+
         @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
             let target = (state.units == .mgdL ? preset.target : preset.target?.decimalValue.asMmolL as NSDecimalNumber?) ?? 0
-
             let duration = (preset.duration ?? 0) as Decimal
-            let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
+            let name = preset.name ?? ""
             let percent = preset.percentage / 100
             let perpetual = preset.indefinite
-            let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
-            let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
-            let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
-            let targetString = target != 0 ? target.description : ""
+            let durationString = perpetual ? "" : "\(formatHrMin(Int(duration)))"
+            let scheduledSMBstring = preset.smbIsScheduledOff && preset.start != preset.end
+                ? " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
+                : ""
+            let smbString = (preset.smbIsOff || preset.smbIsScheduledOff) ? "SMBs Off\(scheduledSMBstring)" : ""
+            let targetString = target != 0 ? "\(target.description) \(state.units.rawValue)" : ""
             let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
             let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
-            let isfString = preset.isf ? "ISF" : ""
-            let crString = preset.cr ? "CR" : ""
-            let dash = crString != "" ? "/" : ""
-            let isfAndCRstring = isfString + dash + crString
+            let maxSmbMinsString = (
+                maxMinutesSMB != 0 && preset.advancedSettings && !preset.smbIsOff && maxMinutesSMB != state
+                    .defaultSmbMinutes
+            ) ?
+                "\(maxMinutesSMB.formatted()) min SMB" : ""
+            let maxUamMinsString = (
+                maxMinutesUAM != 0 && preset.advancedSettings && !preset.smbIsOff && maxMinutesUAM != state
+                    .defaultUamMinutes
+            ) ?
+                "\(maxMinutesUAM.formatted()) min UAM" : ""
+            let isfAndCRstring: String = {
+                switch (preset.isfAndCr, preset.isf, preset.cr) {
+                case (_, true, true),
+                     (true, _, _):
+                    return " ISF/CR"
+                case (false, true, false):
+                    return " ISF"
+                case (false, false, true):
+                    return " CR"
+                default:
+                    return ""
+                }
+            }()
             let isSelected = preset.id == selectedPresetID
 
-            if name != "" {
-                ZStack(alignment: .trailing, content: {
+            let labels: [String] = [
+                durationString,
+                percent != 1 ? "\(Int(percent * 100))%\(isfAndCRstring)" : "",
+                targetString,
+                smbString,
+                maxSmbMinsString,
+                maxUamMinsString
+            ].filter { !$0.isEmpty } // filter out empty labels
+
+            if !name.isEmpty {
+                ZStack(alignment: .trailing) {
                     HStack {
                         VStack {
                             HStack {
@@ -499,18 +572,11 @@ extension OverrideConfig {
                                 Spacer()
                             }
                             HStack(spacing: 5) {
-                                Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
-                                if targetString != "" {
-                                    Text(targetString)
-                                    Text(targetString != "" ? state.units.rawValue : "")
-                                }
-                                if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
-                                if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
-                                if scheduledSMBstring != "" { Text(scheduledSMBstring) }
-                                if preset.advancedSettings {
-                                    Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
-                                    Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
-                                    Text(isfAndCRstring)
+                                ForEach(labels, id: \.self) { label in
+                                    Text(label)
+                                    if label != labels.last { // Add divider between labels
+                                        overrideLabelDivider
+                                    }
                                 }
                                 Spacer()
                             }
@@ -527,7 +593,7 @@ extension OverrideConfig {
                                 showCheckmark.toggle()
                                 selectedPresetID = preset.id
 
-                                // deactivate showCheckmark after 3 seconds
+                                // Deactivate checkmark after 3 seconds
                                 DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                                     showCheckmark = false
                                 }
@@ -545,7 +611,7 @@ extension OverrideConfig {
                             .imageScale(.medium)
                             .foregroundStyle(.secondary)
                     }
-                })
+                }
             }
         }
     }

+ 1 - 1
Model/Classes+Properties/OverrideStored+CoreDataProperties.swift

@@ -21,7 +21,7 @@ public extension OverrideStored {
     @NSManaged var name: String?
     @NSManaged var orderPosition: Int16
     @NSManaged var percentage: Double
-    @NSManaged var smbIsAlwaysOff: Bool
+    @NSManaged var smbIsScheduledOff: Bool
     @NSManaged var smbIsOff: Bool
     @NSManaged var smbMinutes: NSDecimalNumber?
     @NSManaged var start: NSDecimalNumber?

+ 2 - 2
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -152,8 +152,8 @@
         <attribute name="name" optional="YES" attributeType="String"/>
         <attribute name="orderPosition" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="percentage" optional="YES" attributeType="Double" defaultValueString="100" usesScalarValueType="YES"/>
-        <attribute name="smbIsAlwaysOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="smbIsOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
+        <attribute name="smbIsScheduledOff" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="smbMinutes" optional="YES" attributeType="Decimal" defaultValueString="30"/>
         <attribute name="start" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="target" optional="YES" attributeType="Decimal" defaultValueString="0"/>
@@ -225,4 +225,4 @@
             <fetchIndexElement property="isPreset" type="Binary" order="descending"/>
         </fetchIndex>
     </entity>
-</model>
+</model>