فهرست منبع

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

Enable editing of preset profile overrides
Deniz Cengiz 1 سال پیش
والد
کامیت
cba2bf8dd1

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

@@ -29,6 +29,24 @@ extension OverrideProfilesConfig {
 
         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() {
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
@@ -38,6 +56,59 @@ extension OverrideProfilesConfig {
 
         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() {
             coredataContext.perform { [self] in
                 let saveOverride = Override(context: self.coredataContext)
@@ -167,7 +238,6 @@ extension OverrideProfilesConfig {
                 let requestEnabled = Override.fetchRequest() as NSFetchRequest<Override>
                 let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
                 requestEnabled.sortDescriptors = [sortIsEnabled]
-                // requestEnabled.fetchLimit = 1
                 try? overrideArray = coredataContext.fetch(requestEnabled)
                 isEnabled = overrideArray.first?.enabled ?? false
                 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() {
             _indefinite = true
             isEnabled = false
@@ -248,5 +341,26 @@ extension OverrideProfilesConfig {
             smbMinutes = defaultSmbMinutes
             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 Foundation
 import SwiftUI
 import Swinject
 
@@ -10,8 +11,14 @@ extension OverrideProfilesConfig {
         @State private var isEditing = false
         @State private var showAlert = false
         @State private var showingDetail = false
+        @State private var selectedPreset: OverridePresets?
+        @State private var isEditSheetPresented: Bool = false
         @State private var alertSring = ""
         @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(\.managedObjectContext) var moc
@@ -22,6 +29,7 @@ extension OverrideProfilesConfig {
                 format: "name != %@", "" as String
             )
         ) var fetchedProfiles: FetchedResults<OverridePresets>
+        var units: GlucoseUnits = .mmolL
 
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -43,144 +51,246 @@ extension OverrideProfilesConfig {
 
         var presetPopover: some View {
             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 {
                     Button("Save") {
                         state.savePreset()
                         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") {
                         isSheetPresented = false
                     }
+                    .tint(.red)
                 }
             }
         }
 
-        var body: some View {
+        var editPresetPopover: some View {
             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 {
-                        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 {
-                        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 {
-                            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 {
-                                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 {
-                                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 {
-                            Text("SMB Minutes")
-                            DecimalTextField(
-                                "0",
-                                value: $state.smbMinutes,
-                                formatter: formatter,
-                                cleanInput: false
-                            )
-                            Text("minutes").foregroundColor(.secondary)
+                            Toggle(isOn: $state.isf) {
+                                Text("Change ISF")
+                            }
                         }
                         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 {
                         Button("Start new Profile") {
                             showAlert.toggle()
@@ -244,6 +354,7 @@ extension OverrideProfilesConfig {
                             .tint(.orange)
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .buttonStyle(BorderlessButtonStyle())
+                            .font(.callout)
                             .controlSize(.mini)
                             .disabled(unChanged())
                     }
@@ -251,8 +362,6 @@ extension OverrideProfilesConfig {
                         presetPopover
                     }
                 }
-
-                header: { Text("Insulin") }
                 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."
@@ -273,46 +382,47 @@ extension OverrideProfilesConfig {
             .navigationBarTitle("Profiles")
             .navigationBarTitleDisplayMode(.automatic)
             .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 {
-            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 {
                     VStack {
                         HStack {
-                            Text(name)
+                            Text(data.name)
                             Spacer()
                         }
                         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 {
-                                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()
                         }
@@ -339,6 +449,39 @@ extension OverrideProfilesConfig {
             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) {
             for index in offsets {
                 let language = fetchedProfiles[index]