Browse Source

Merge pull request #235 from dsnallfot/dev-edit-overrides

Enable editing of preset profile overrides
Deniz Cengiz 1 year ago
parent
commit
cba2bf8dd1

+ 115 - 1
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -29,6 +29,24 @@ extension OverrideProfilesConfig {
 
 
         var units: GlucoseUnits = .mmolL
         var units: GlucoseUnits = .mmolL
 
 
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
+            return formatter
+        }
+
+        private var glucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 0
+            if units == .mmolL {
+                formatter.maximumFractionDigits = 1
+            }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
@@ -38,6 +56,59 @@ extension OverrideProfilesConfig {
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
+        struct ProfileViewData {
+            let target: Decimal
+            let duration: Decimal
+            let name: String
+            let percent: Double
+            let perpetual: Bool
+            let durationString: String
+            let scheduledSMBString: String
+            let smbString: String
+            let targetString: String
+            let maxMinutesSMB: Decimal
+            let maxMinutesUAM: Decimal
+            let isfString: String
+            let crString: String
+            let isfAndCRString: String
+        }
+
+        func profileViewData(for preset: OverridePresets) -> ProfileViewData {
+            let target = units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
+                .asMmolL : (preset.target ?? 0) as Decimal
+            let duration = (preset.duration ?? 0) as Decimal
+            let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : 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.smbIsScheduledOff) ? "Scheduled SMBs" : ""
+            let smbString = (preset.smbIsOff && scheduledSMBString == "") ? "SMBs are off" : ""
+            let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
+            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
+
+            return ProfileViewData(
+                target: target,
+                duration: duration,
+                name: name,
+                percent: percent,
+                perpetual: perpetual,
+                durationString: durationString,
+                scheduledSMBString: scheduledSMBString,
+                smbString: smbString,
+                targetString: targetString,
+                maxMinutesSMB: maxMinutesSMB,
+                maxMinutesUAM: maxMinutesUAM,
+                isfString: isfString,
+                crString: crString,
+                isfAndCRString: isfAndCRString
+            )
+        }
+
         func saveSettings() {
         func saveSettings() {
             coredataContext.perform { [self] in
             coredataContext.perform { [self] in
                 let saveOverride = Override(context: self.coredataContext)
                 let saveOverride = Override(context: self.coredataContext)
@@ -167,7 +238,6 @@ extension OverrideProfilesConfig {
                 let requestEnabled = Override.fetchRequest() as NSFetchRequest<Override>
                 let requestEnabled = Override.fetchRequest() as NSFetchRequest<Override>
                 let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
                 let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
                 requestEnabled.sortDescriptors = [sortIsEnabled]
                 requestEnabled.sortDescriptors = [sortIsEnabled]
-                // requestEnabled.fetchLimit = 1
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 isEnabled = overrideArray.first?.enabled ?? false
                 isEnabled = overrideArray.first?.enabled ?? false
                 percentage = overrideArray.first?.percentage ?? 100
                 percentage = overrideArray.first?.percentage ?? 100
@@ -230,6 +300,29 @@ extension OverrideProfilesConfig {
             }
             }
         }
         }
 
 
+        func populateSettings(from preset: OverridePresets) {
+            profileName = preset.name ?? ""
+            percentage = preset.percentage
+            duration = (preset.duration ?? 0) as Decimal
+            _indefinite = preset.indefinite
+            override_target = preset.target != nil
+            if let targetValue = preset.target as NSDecimalNumber? {
+                target = units == .mmolL ? (targetValue as Decimal).asMmolL : targetValue as Decimal
+            } else {
+                target = 0
+            }
+            advancedSettings = preset.advancedSettings
+            smbIsOff = preset.smbIsOff
+            smbIsScheduledOff = preset.smbIsScheduledOff
+            isf = preset.isf
+            cr = preset.cr
+            smbMinutes = (preset.smbMinutes ?? 0) as Decimal
+            uamMinutes = (preset.uamMinutes ?? 0) as Decimal
+            isfAndCr = preset.isfAndCr
+            start = (preset.start ?? 0) as Decimal
+            end = (preset.end ?? 0) as Decimal
+        }
+
         func cancelProfile() {
         func cancelProfile() {
             _indefinite = true
             _indefinite = true
             isEnabled = false
             isEnabled = false
@@ -248,5 +341,26 @@ extension OverrideProfilesConfig {
             smbMinutes = defaultSmbMinutes
             smbMinutes = defaultSmbMinutes
             uamMinutes = defaultUamMinutes
             uamMinutes = defaultUamMinutes
         }
         }
+
+        func updatePreset(_ preset: OverridePresets) {
+            let context = CoreDataStack.shared.persistentContainer.viewContext
+            context.performAndWait {
+                preset.name = profileName
+                preset.percentage = percentage
+                preset.duration = NSDecimalNumber(decimal: duration)
+                let targetValue = override_target ? (units == .mmolL ? target.asMgdL : target) : nil
+                preset.target = targetValue != nil ? NSDecimalNumber(decimal: targetValue!) : nil
+                preset.indefinite = _indefinite
+                preset.advancedSettings = advancedSettings
+                preset.smbIsOff = smbIsOff
+                preset.smbIsScheduledOff = smbIsScheduledOff
+                preset.isf = isf
+                preset.cr = cr
+                preset.smbMinutes = NSDecimalNumber(decimal: smbMinutes)
+                preset.uamMinutes = NSDecimalNumber(decimal: uamMinutes)
+                preset.isfAndCr = isfAndCr
+                try? context.save()
+            }
+        }
     }
     }
 }
 }

+ 271 - 128
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -1,4 +1,5 @@
 import CoreData
 import CoreData
+import Foundation
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -10,8 +11,14 @@ extension OverrideProfilesConfig {
         @State private var isEditing = false
         @State private var isEditing = false
         @State private var showAlert = false
         @State private var showAlert = false
         @State private var showingDetail = false
         @State private var showingDetail = false
+        @State private var selectedPreset: OverridePresets?
+        @State private var isEditSheetPresented: Bool = false
         @State private var alertSring = ""
         @State private var alertSring = ""
         @State var isSheetPresented: Bool = false
         @State var isSheetPresented: Bool = false
+        @State private var originalPreset: OverridePresets?
+        @State private var showDeleteAlert = false
+        @State private var indexToDelete: Int?
+        @State private var profileNameToDelete: String = ""
 
 
         @Environment(\.dismiss) var dismiss
         @Environment(\.dismiss) var dismiss
         @Environment(\.managedObjectContext) var moc
         @Environment(\.managedObjectContext) var moc
@@ -22,6 +29,7 @@ extension OverrideProfilesConfig {
                 format: "name != %@", "" as String
                 format: "name != %@", "" as String
             )
             )
         ) var fetchedProfiles: FetchedResults<OverridePresets>
         ) var fetchedProfiles: FetchedResults<OverridePresets>
+        var units: GlucoseUnits = .mmolL
 
 
         private var formatter: NumberFormatter {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
@@ -43,144 +51,246 @@ extension OverrideProfilesConfig {
 
 
         var presetPopover: some View {
         var presetPopover: some View {
             Form {
             Form {
-                Section {
-                    TextField("Name Of Profile", text: $state.profileName)
-                } header: { Text("Enter Name of Profile") }
-
+                nameSection(header: "Enter a name")
+                settingsSection(header: "Settings to save")
                 Section {
                 Section {
                     Button("Save") {
                     Button("Save") {
                         state.savePreset()
                         state.savePreset()
                         isSheetPresented = false
                         isSheetPresented = false
                     }
                     }
-                    .disabled(state.profileName.isEmpty || fetchedProfiles.filter({ $0.name == state.profileName }).isNotEmpty)
+                    .disabled(
+                        state.profileName.isEmpty || fetchedProfiles
+                            .contains(where: { $0.name == state.profileName })
+                    )
 
 
                     Button("Cancel") {
                     Button("Cancel") {
                         isSheetPresented = false
                         isSheetPresented = false
                     }
                     }
+                    .tint(.red)
                 }
                 }
             }
             }
         }
         }
 
 
-        var body: some View {
+        var editPresetPopover: some View {
             Form {
             Form {
-                if state.presets.isNotEmpty {
-                    Section {
-                        ForEach(fetchedProfiles) { preset in
-                            profilesView(for: preset)
-                        }.onDelete(perform: removeProfile)
+                nameSection(header: "Change name?")
+                settingsConfig(header: "Change settings")
+                Section {
+                    Button("Save") {
+                        guard let selectedPreset = selectedPreset else { return }
+                        state.updatePreset(selectedPreset)
+                        isEditSheetPresented = false
+                    }
+                    .disabled(!hasChanges())
+
+                    Button("Cancel") {
+                        isEditSheetPresented = false
                     }
                     }
+                    .tint(.red)
                 }
                 }
-                Section {
-                    VStack {
-                        Slider(
-                            value: $state.percentage,
-                            in: 10 ... 200,
-                            step: 1,
-                            onEditingChanged: { editing in
-                                isEditing = editing
-                            }
-                        ).accentColor(state.percentage >= 130 ? .red : .blue)
-                        Text("\(state.percentage.formatted(.number)) %")
-                            .foregroundColor(
-                                state
-                                    .percentage >= 130 ? .red :
-                                    (isEditing ? .orange : .blue)
-                            )
-                            .font(.largeTitle)
-                        Spacer()
-                        Toggle(isOn: $state._indefinite) {
-                            Text("Enable indefinitely")
+            }
+            .onAppear {
+                if let preset = selectedPreset {
+                    originalPreset = preset
+                    state.populateSettings(from: preset)
+                }
+            }
+            .onDisappear {
+                state.savedSettings()
+            }
+        }
+
+        @ViewBuilder private func nameSection(header: String) -> some View {
+            Section {
+                TextField("Profile override name", text: $state.profileName)
+            } header: {
+                Text(header)
+            }
+        }
+
+        @ViewBuilder private func settingsConfig(header: String) -> some View {
+            Section {
+                VStack {
+                    Spacer()
+                    Text("\(state.percentage.formatted(.number)) %")
+                        .foregroundColor(
+                            state
+                                .percentage >= 130 ? .red :
+                                (isEditing ? .orange : .blue)
+                        )
+                        .font(.largeTitle)
+                    Slider(
+                        value: $state.percentage,
+                        in: 10 ... 200,
+                        step: 1,
+                        onEditingChanged: { editing in
+                            isEditing = editing
                         }
                         }
+                    ).accentColor(state.percentage >= 130 ? .red : .blue)
+                    Spacer()
+                    Toggle(isOn: $state._indefinite) {
+                        Text("Enable indefinitely")
                     }
                     }
-                    if !state._indefinite {
-                        HStack {
-                            Text("Duration")
-                            DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
-                            Text("minutes").foregroundColor(.secondary)
-                        }
+                }
+                if !state._indefinite {
+                    HStack {
+                        Text("Duration")
+                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
+                        Text("minutes").foregroundColor(.secondary)
                     }
                     }
+                }
 
 
+                HStack {
+                    Toggle(isOn: $state.override_target) {
+                        Text("Override Profile Target")
+                    }
+                }
+                if state.override_target {
                     HStack {
                     HStack {
-                        Toggle(isOn: $state.override_target) {
-                            Text("Override Profile Target")
-                        }
+                        Text("Target Glucose")
+                        DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
+                        Text(state.units.rawValue).foregroundColor(.secondary)
                     }
                     }
-                    if state.override_target {
-                        HStack {
-                            Text("Target Glucose")
-                            DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
-                            Text(state.units.rawValue).foregroundColor(.secondary)
-                        }
+                }
+                HStack {
+                    Toggle(isOn: $state.advancedSettings) {
+                        Text("More options")
                     }
                     }
+                }
+                if state.advancedSettings {
                     HStack {
                     HStack {
-                        Toggle(isOn: $state.advancedSettings) {
-                            Text("More options")
+                        Toggle(isOn: $state.smbIsOff) {
+                            Text("Always Disable SMBs")
                         }
                         }
                     }
                     }
-                    if state.advancedSettings {
-                        HStack {
-                            Toggle(isOn: $state.smbIsOff) {
-                                Text("Always Disable SMBs")
-                            }
-                        }
-                        if !state.smbIsOff {
-                            HStack {
-                                Toggle(isOn: $state.smbIsScheduledOff) {
-                                    Text("Schedule when SMBs are Off")
-                                }
-                            }
-                            if state.smbIsScheduledOff {
-                                HStack {
-                                    Text("First Hour SMBs are Off (24 hours)")
-                                    DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
-                                    Text("hour").foregroundColor(.secondary)
-                                }
-                                HStack {
-                                    Text("First Hour SMBs are Resumed (24 hours)")
-                                    DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
-                                    Text("hour").foregroundColor(.secondary)
-                                }
-                            }
-                        }
+                    if !state.smbIsOff {
                         HStack {
                         HStack {
-                            Toggle(isOn: $state.isfAndCr) {
-                                Text("Change ISF and CR")
+                            Toggle(isOn: $state.smbIsScheduledOff) {
+                                Text("Schedule when SMBs are Off")
                             }
                             }
                         }
                         }
-                        if !state.isfAndCr {
+                        if state.smbIsScheduledOff {
                             HStack {
                             HStack {
-                                Toggle(isOn: $state.isf) {
-                                    Text("Change ISF")
-                                }
+                                Text("First Hour SMBs are Off (24 hours)")
+                                DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
+                                Text("hour").foregroundColor(.secondary)
                             }
                             }
                             HStack {
                             HStack {
-                                Toggle(isOn: $state.cr) {
-                                    Text("Change CR")
-                                }
+                                Text("First Hour SMBs are Resumed (24 hours)")
+                                DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
+                                Text("hour").foregroundColor(.secondary)
                             }
                             }
                         }
                         }
+                    }
+                    HStack {
+                        Toggle(isOn: $state.isfAndCr) {
+                            Text("Change ISF and CR")
+                        }
+                    }
+                    if !state.isfAndCr {
                         HStack {
                         HStack {
-                            Text("SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.smbMinutes,
-                                formatter: formatter,
-                                cleanInput: false
-                            )
-                            Text("minutes").foregroundColor(.secondary)
+                            Toggle(isOn: $state.isf) {
+                                Text("Change ISF")
+                            }
                         }
                         }
                         HStack {
                         HStack {
-                            Text("UAM SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.uamMinutes,
-                                formatter: formatter,
-                                cleanInput: false
-                            )
-                            Text("minutes").foregroundColor(.secondary)
+                            Toggle(isOn: $state.cr) {
+                                Text("Change CR")
+                            }
                         }
                         }
                     }
                     }
+                    HStack {
+                        Text("SMB Minutes")
+                        DecimalTextField(
+                            "0",
+                            value: $state.smbMinutes,
+                            formatter: formatter,
+                            cleanInput: false
+                        )
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("UAM SMB Minutes")
+                        DecimalTextField(
+                            "0",
+                            value: $state.uamMinutes,
+                            formatter: formatter,
+                            cleanInput: false
+                        )
+                        Text("minutes").foregroundColor(.secondary)
+                    }
+                }
+            } header: {
+                Text(header)
+            }
+        }
+
+        @ViewBuilder private func settingsSection(header: String) -> some View {
+            Section(header: Text(header)) {
+                let percentString = Text("Override: \(Int(state.percentage))%")
+                let targetString = state
+                    .target != 0 ? Text("Target: \(state.target.formatted()) \(state.units.rawValue)") : Text("")
+                let durationString = state
+                    ._indefinite ? Text("Duration: Indefinite") : Text("Duration: \(state.duration.formatted()) minutes")
+                let isfString = state.isf ? Text("Change ISF") : Text("")
+                let crString = state.cr ? Text("Change CR") : Text("")
+                let smbString = state.smbIsOff ? Text("Disable SMB") : Text("")
+                let scheduledSMBString = state.smbIsScheduledOff ? Text("SMB Schedule On") : Text("")
+                let maxMinutesSMBString = state
+                    .smbMinutes != 0 ? Text("\(state.smbMinutes.formatted()) SMB Basal minutes") : Text("")
+                let maxMinutesUAMString = state
+                    .uamMinutes != 0 ? Text("\(state.uamMinutes.formatted()) UAM Basal minutes") : Text("")
+
+                VStack(alignment: .leading, spacing: 2) {
+                    percentString
+                    if targetString != Text("") { targetString }
+                    if durationString != Text("") { durationString }
+                    if isfString != Text("") { isfString }
+                    if crString != Text("") { crString }
+                    if smbString != Text("") { smbString }
+                    if scheduledSMBString != Text("") { scheduledSMBString }
+                    if maxMinutesSMBString != Text("") { maxMinutesSMBString }
+                    if maxMinutesUAMString != Text("") { maxMinutesUAMString }
+                }
+                .foregroundColor(.secondary)
+                .font(.caption)
+            }
+        }
+
+        var body: some View {
+            Form {
+                if state.presets.isNotEmpty {
+                    Section {
+                        ForEach(fetchedProfiles.indices, id: \.self) { index in
+                            let preset = fetchedProfiles[index]
+                            profilesView(for: preset)
+                                .swipeActions {
+                                    Button(role: .none) {
+                                        indexToDelete = index
+                                        profileNameToDelete = preset.name ?? "this profile"
+                                        showDeleteAlert = true
+                                    } label: {
+                                        Label("Delete", systemImage: "trash")
+                                    }.tint(.red)
 
 
+                                    Button {
+                                        selectedPreset = preset
+                                        state.profileName = preset.name ?? ""
+                                        isEditSheetPresented = true
+                                    } label: {
+                                        Label("Edit", systemImage: "square.and.pencil")
+                                    }.tint(.blue)
+                                }
+                        }
+                    }
+                    header: { Text("Activate profile override") }
+                    footer: { VStack(alignment: .leading) {
+                        Text("Swipe left on a profile to edit or delete it.")
+                    }
+                    }
+                }
+                settingsConfig(header: "Insulin")
+                Section {
                     HStack {
                     HStack {
                         Button("Start new Profile") {
                         Button("Start new Profile") {
                             showAlert.toggle()
                             showAlert.toggle()
@@ -244,6 +354,7 @@ extension OverrideProfilesConfig {
                             .tint(.orange)
                             .tint(.orange)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .buttonStyle(BorderlessButtonStyle())
                             .buttonStyle(BorderlessButtonStyle())
+                            .font(.callout)
                             .controlSize(.mini)
                             .controlSize(.mini)
                             .disabled(unChanged())
                             .disabled(unChanged())
                     }
                     }
@@ -251,8 +362,6 @@ extension OverrideProfilesConfig {
                         presetPopover
                         presetPopover
                     }
                     }
                 }
                 }
-
-                header: { Text("Insulin") }
                 footer: {
                 footer: {
                     Text(
                     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."
                         "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
@@ -273,46 +382,47 @@ extension OverrideProfilesConfig {
             .navigationBarTitle("Profiles")
             .navigationBarTitle("Profiles")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .sheet(isPresented: $isEditSheetPresented) {
+                editPresetPopover
+                    .padding()
+            }
+            .alert(isPresented: $showDeleteAlert) {
+                Alert(
+                    title: Text("Delete profile override"),
+                    message: Text("Are you sure you want to delete\n\(profileNameToDelete)?"),
+                    primaryButton: .destructive(Text("Delete")) {
+                        if let index = indexToDelete {
+                            removeProfile(at: IndexSet(integer: index))
+                        }
+                    },
+                    secondaryButton: .cancel()
+                )
+            }
         }
         }
 
 
         @ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
         @ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
-            let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
-                .asMmolL : (preset.target ?? 0) as Decimal
-            let duration = (preset.duration ?? 0) as Decimal
-            let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : 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.smbIsScheduledOff) ? "Scheduled SMBs" : ""
-            let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
-            let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
-            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 data = state.profileViewData(for: preset)
 
 
-            if name != "" {
+            if data.name != "" {
                 HStack {
                 HStack {
                     VStack {
                     VStack {
                         HStack {
                         HStack {
-                            Text(name)
+                            Text(data.name)
                             Spacer()
                             Spacer()
                         }
                         }
                         HStack(spacing: 5) {
                         HStack(spacing: 5) {
-                            Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
-                            if targetString != "" {
-                                Text(targetString)
-                                Text(targetString != "" ? state.units.rawValue : "")
+                            Text(data.percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
+                            if data.targetString != "" {
+                                Text(data.targetString)
+                                Text(data.targetString != "" ? state.units.rawValue : "")
                             }
                             }
-                            if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
-                            if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
-                            if scheduledSMBstring != "" { Text(scheduledSMBstring) }
+                            if data.durationString != "" { Text(data.durationString + (data.perpetual ? "" : "min")) }
+                            if data.smbString != "" { Text(data.smbString).foregroundColor(.secondary).font(.caption) }
+                            if data.scheduledSMBString != "" { Text(data.scheduledSMBString) }
                             if preset.advancedSettings {
                             if preset.advancedSettings {
-                                Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
-                                Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
-                                Text(isfAndCRstring)
+                                Text(data.maxMinutesSMB == 0 ? "" : data.maxMinutesSMB.formatted() + " SMB")
+                                Text(data.maxMinutesUAM == 0 ? "" : data.maxMinutesUAM.formatted() + " UAM")
+                                Text(data.isfAndCRString)
                             }
                             }
                             Spacer()
                             Spacer()
                         }
                         }
@@ -339,6 +449,39 @@ extension OverrideProfilesConfig {
             return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
             return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
         }
         }
 
 
+        private func hasChanges() -> Bool {
+            guard let originalPreset = originalPreset else { return false }
+
+            let targetInStateUnits: Decimal
+            let targetInPresetUnits: Decimal
+
+            if state.units == .mmolL {
+                targetInStateUnits = state.target
+                targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue.asMmolL ?? 0
+            } else {
+                targetInStateUnits = state.target
+                targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue ?? 0
+            }
+
+            let hasChanges = state.profileName != originalPreset.name ||
+                state.percentage != originalPreset.percentage ||
+                state.duration != (originalPreset.duration ?? 0) as Decimal ||
+                state._indefinite != originalPreset.indefinite ||
+                state.override_target != (originalPreset.target != nil) ||
+                (state.override_target && targetInStateUnits != targetInPresetUnits) ||
+                state.smbIsOff != originalPreset.smbIsOff ||
+                state.smbIsScheduledOff != originalPreset.smbIsScheduledOff ||
+                state.isf != originalPreset.isf ||
+                state.cr != originalPreset.cr ||
+                state.smbMinutes != (originalPreset.smbMinutes ?? 0) as Decimal ||
+                state.uamMinutes != (originalPreset.uamMinutes ?? 0) as Decimal ||
+                state.isfAndCr != originalPreset.isfAndCr ||
+                state.start != (originalPreset.start ?? 0) as Decimal ||
+                state.end != (originalPreset.end ?? 0) as Decimal
+
+            return hasChanges
+        }
+
         private func removeProfile(at offsets: IndexSet) {
         private func removeProfile(at offsets: IndexSet) {
             for index in offsets {
             for index in offsets {
                 let language = fetchedProfiles[index]
                 let language = fetchedProfiles[index]