Просмотр исходного кода

several logic fixes, refactoring (a lot) and docstrings

polscm32 aka Marvout 1 год назад
Родитель
Сommit
d30da48873

+ 221 - 103
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -18,8 +18,6 @@ extension DataTable {
 
         var mode: Mode = .treatments
         var treatments: [Treatment] = []
-        var glucose: [Glucose] = []
-        var meals: [Treatment] = []
         var manualGlucose: Decimal = 0
         var waitForSuggestion: Bool = false
 
@@ -37,12 +35,15 @@ extension DataTable {
             broadcaster.register(SettingsObserver.self, observer: self)
         }
 
+        /// Checks if the glucose data is fresh based on the given date
+        /// - Parameter glucoseDate: The date to check
+        /// - Returns: Boolean indicating if the data is fresh
         func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
 
-        // Glucose deletion from history and from remote services
-        /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        /// Initiates the glucose deletion process asynchronously
+        /// - Parameter treatmentObjectID: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
                 await deleteGlucose(treatmentObjectID)
@@ -124,60 +125,6 @@ extension DataTable {
             await apsManager.determineBasalSync()
         }
 
-        func updateCarbEntry(_ treatmentObjectID: NSManagedObjectID, newAmount: Decimal, newNote: String) {
-            Task {
-                // Update carb entry in Core Data
-                await updateCarbEntryInCoreData(treatmentObjectID, newAmount: newAmount, newNote: newNote)
-
-                // Perform a determine basal sync to keep data up to date
-                await apsManager.determineBasalSync()
-
-                // Delete carbs from Services
-                await deleteCarbsFromServices(treatmentObjectID)
-
-                // Upload updated carb entry to services in parallel
-                async let nightscoutUpload: () = self.provider.nightscoutManager.uploadCarbs()
-                async let healthKitUpload: () = self.provider.healthkitManager.uploadCarbs()
-                async let tidepoolUpload: () = self.provider.tidepoolManager.uploadCarbs()
-
-                // Wait for all uploads to complete
-                _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
-            }
-        }
-
-        private func updateCarbEntryInCoreData(
-            _ treatmentObjectID: NSManagedObjectID,
-            newAmount: Decimal,
-            newNote: String
-        ) async {
-            let context = CoreDataStack.shared.newTaskContext()
-            context.name = "updateContext"
-            context.transactionAuthor = "updateCarbEntry"
-
-            await context.perform {
-                do {
-                    if let carbToUpdate = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored {
-                        carbToUpdate.carbs = Double(newAmount)
-                        carbToUpdate.note = newNote
-                        carbToUpdate.isUploadedToNS = false
-                        carbToUpdate.isUploadedToHealth = false
-                        carbToUpdate.isUploadedToTidepool = false
-
-                        guard context.hasChanges else { return }
-                        try context.save()
-
-                        debugPrint(
-                            "\(DebuggingIdentifiers.succeeded) Updated Carb Entry in Core Data"
-                        )
-                    }
-                } catch {
-                    debugPrint(
-                        "\(DebuggingIdentifiers.failed) Error updating carb entry in Core Data with error: \(error.localizedDescription)"
-                    )
-                }
-            }
-        }
-
         func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
@@ -304,7 +251,190 @@ extension DataTable {
             }
         }
 
-        // Function to get the original zero-carb non-FPU entry
+        // MARK: - Entry Management
+
+        /// Updates a carb/FPU entry with new values and handles the necessary cleanup and recreation of FPU entries
+        /// - Parameters:
+        ///   - treatmentObjectID: The ID of the entry to update
+        ///   - newCarbs: The new carbs value
+        ///   - newFat: The new fat value
+        ///   - newProtein: The new protein value
+        ///   - newNote: The new note text
+        func updateEntry(
+            _ treatmentObjectID: NSManagedObjectID,
+            newCarbs: Decimal,
+            newFat: Decimal,
+            newProtein: Decimal,
+            newNote: String
+        ) {
+            Task {
+                let originalDate = await getOriginalEntryDate(treatmentObjectID)
+                await updateEntryInCoreData(treatmentObjectID, newCarbs: newCarbs, newNote: newNote)
+                await deleteOldAndCreateNewFPUEntry(
+                    treatmentObjectID: treatmentObjectID,
+                    originalDate: originalDate,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+                await syncWithServices()
+            }
+        }
+
+        /// Retrieves the original date of an entry and sets the isFPU flag
+        /// - Parameter objectID: The ID of the entry
+        /// - Returns: The original date or current date if not found
+        private func getOriginalEntryDate(_ objectID: NSManagedObjectID) async -> Date {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "updateContext"
+            context.transactionAuthor = "updateEntry"
+
+            return await context.perform {
+                do {
+                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored
+                    else { return Date() }
+
+                    /// Hacky workaround: Set isFPU flag to true before deletion
+                    /// This is necessary because the deleteCarbs function in the CarbsStorage will fail if the isFPU flag is false and the entry won't get deleted.
+                    entry.isFPU = true
+                    try context.save()
+
+                    return entry.date ?? Date()
+                } catch {
+                    return Date()
+                }
+            }
+        }
+
+        /// Updates a carb entry in Core Data
+        /// The FPU entries are deleted and recreated. We don't need to do this for the carb entries as we can simply update the carb entry in Core Data.
+        /// - Parameters:
+        ///   - objectID: The ID of the entry to update
+        ///   - newCarbs: The new carbs value
+        ///   - newNote: The new note text
+        private func updateEntryInCoreData(
+            _ objectID: NSManagedObjectID,
+            newCarbs: Decimal,
+            newNote: String
+        ) async {
+            let context = CoreDataStack.shared.newTaskContext()
+
+            await context.perform {
+                do {
+                    let entry = try context.existingObject(with: objectID) as? CarbEntryStored
+                    entry?.carbs = Double(newCarbs)
+                    entry?.note = newNote
+                    try context.save()
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to update entry: \(error.localizedDescription)")
+                }
+            }
+        }
+
+        /// Deletes the old FPU entry and creates a new one with updated values
+        /// - Parameters:
+        ///   - treatmentObjectID: The ID of the entry to delete
+        ///   - originalDate: The original date to preserve
+        ///   - newCarbs: The new carbs value
+        ///   - newFat: The new fat value
+        ///   - newProtein: The new protein value
+        ///   - newNote: The new note text
+        private func deleteOldAndCreateNewFPUEntry(
+            treatmentObjectID: NSManagedObjectID,
+            originalDate: Date,
+            newCarbs: Decimal,
+            newFat: Decimal,
+            newProtein: Decimal,
+            newNote: String
+        ) async {
+            // Delete old FPU entry from Core Data and Remote Services and await this
+            await deleteCarbs(treatmentObjectID)
+
+            // Create new FPU entry
+            let newEntry = CarbsEntry(
+                id: UUID().uuidString,
+                createdAt: Date(),
+                actualDate: originalDate,
+                carbs: newCarbs,
+                fat: newFat,
+                protein: newProtein,
+                note: newNote,
+                enteredBy: CarbsEntry.local,
+                isFPU: true,
+                fpuID: UUID().uuidString
+            )
+
+            await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+        }
+
+        /// Synchronizes the FPU/ Carb entry with all remote services in parallel
+        private func syncWithServices() async {
+            async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
+            async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
+            async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
+
+            _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
+        }
+
+        // MARK: - Entry Loading
+
+        /// Loads the values of a carb or FPU entry from Core Data
+        /// - Parameter objectID: The ID of the entry to load
+        /// - Returns: A tuple containing the entry's values, or nil if not found
+        func loadEntryValues(from objectID: NSManagedObjectID) async
+            -> (carbs: Decimal, fat: Decimal, protein: Decimal, note: String)?
+        {
+            let context = CoreDataStack.shared.persistentContainer.viewContext
+
+            return await context.perform {
+                do {
+                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored else { return nil }
+                    return (
+                        carbs: Decimal(entry.carbs),
+                        fat: Decimal(entry.fat),
+                        protein: Decimal(entry.protein),
+                        note: entry.note ?? ""
+                    )
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error.localizedDescription)")
+                    return nil
+                }
+            }
+        }
+
+        // MARK: - FPU Entry Handling
+
+        /// Handles the loading of FPU entries based on their type
+        /// If the user taps on an FPU entry in the DataTable list, there are two cases:
+        /// - the User has entered this FPU entry WITH carbs
+        /// - the User has entered this FPU entry WITHOUT carbs
+        /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+        /// In the second case, we need to load the zero-carb entry that actually holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+        /// - Parameter objectID: The ID of the FPU entry
+        /// - Returns: A tuple containing the entry values and ID, or nil if not found
+        func handleFPUEntry(_ objectID: NSManagedObjectID) async
+            -> (entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String)?, entryID: NSManagedObjectID?)?
+        {
+            // Case 1: FPU entry WITH carbs
+            if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
+                if let values = await loadEntryValues(from: correspondingCarbEntryID) {
+                    return (values, correspondingCarbEntryID)
+                }
+            }
+            // Case 2: FPU entry WITHOUT carbs
+            else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
+                if let values = await loadEntryValues(from: originalEntryID) {
+                    return (values, originalEntryID)
+                }
+            }
+            return nil
+        }
+
+        /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
+        /// This is used when the user has entered a FPU entry WITHOUT carbs.
+        /// - Parameter treatmentObjectID: The ID of the FPU entry
+        /// - Returns: The ID of the original entry, or nil if not found
         func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
             let context = CoreDataStack.shared.newTaskContext()
             context.name = "fpuContext"
@@ -338,54 +468,42 @@ extension DataTable {
             }
         }
 
-        func updateEntry(
-            _ treatmentObjectID: NSManagedObjectID,
-            newCarbs: Decimal,
-            newFat: Decimal,
-            newProtein: Decimal,
-            newNote: String
-        ) {
-            Task {
-                // Get the original entry's actualDate before deletion
-                let context = CoreDataStack.shared.newTaskContext()
-
-                let originalDate = await context.perform {
-                    do {
-                        guard let entry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored
-                        else { return Date() }
-                        return entry.date ?? Date()
-                    } catch {
-                        return Date()
-                    }
-                }
-
-                // Delete old FPU from Core Data and Remote Services and await this
-                await deleteCarbs(treatmentObjectID)
+        /// Retrieves the corresponding carb entry for a given FPU entry.
+        /// This is used when the user has entered a carb entry WITH FPUs all at once.
+        /// - Parameter treatmentObjectID: The ID of the FPU entry
+        /// - Returns: The ID of the corresponding carb entry, or nil if not found
+        func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "carbContext"
 
-                // Create new FPU entry with updated values
-                let newEntry = CarbsEntry(
-                    id: UUID().uuidString,
-                    createdAt: Date(),
-                    actualDate: originalDate, // Use the original entry's date
-                    carbs: newCarbs,
-                    fat: newFat,
-                    protein: newProtein,
-                    note: newNote,
-                    enteredBy: CarbsEntry.local,
-                    isFPU: true,
-                    fpuID: UUID().uuidString
-                )
+            return await context.perform {
+                do {
+                    // Get the fpuID from the selected entry
+                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                          let fpuID = selectedEntry.fpuID
+                    else { return nil }
 
-                // Store new entry which will create new FPU entries
-                await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+                    // Fetch the corresponding carb entry with the same fpuID
+                    let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
+                    let request = CarbEntryStored.fetchRequest()
+                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                        NSPredicate(format: "isFPU == NO"),
+                        NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
+                    ])
+                    request.fetchLimit = 1
 
-                // Upload updated entries to services in parallel
-                async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
-                async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
-                async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
+                    let correspondingCarbEntry = try context.fetch(request).first
+                    debugPrint(
+                        "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
+                    )
+                    return correspondingCarbEntry?.objectID
 
-                // Wait for all uploads to complete
-                _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
+                    return nil
+                }
             }
         }
     }

+ 38 - 22
FreeAPS/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -4,6 +4,7 @@
 //
 //  Created by Marvin Polscheit on 15.01.25.
 //
+import CoreData
 import SwiftUI
 
 struct CarbEntryEditorView: View {
@@ -14,6 +15,12 @@ struct CarbEntryEditorView: View {
     var state: DataTable.StateModel
     let carbEntry: CarbEntryStored
 
+    /*
+     This is the objectID of the entry that the user is editing. It is NOT always the `carbEntry: CarbEntryStored` that we pass to the `CarbEntryEditorView`.
+     We need this because FPUs and carbs are treated completely different and that complicates the update process.
+     */
+    @State private var entryToEdit: NSManagedObjectID?
+
     @State private var editedCarbs: Decimal
     @State private var editedFat: Decimal
     @State private var editedProtein: Decimal
@@ -23,11 +30,12 @@ struct CarbEntryEditorView: View {
     init(state: DataTable.StateModel, carbEntry: CarbEntryStored) {
         self.state = state
         self.carbEntry = carbEntry
-        _editedCarbs = State(initialValue: Decimal(carbEntry.carbs))
+        _editedCarbs = State(initialValue: 0)
         _editedFat = State(initialValue: 0) // gets updated in the task block
         _editedProtein = State(initialValue: 0) // gets updated in the task block
         _editedNote = State(initialValue: carbEntry.note ?? "")
         _isFPU = State(initialValue: carbEntry.isFPU)
+        _entryToEdit = State(initialValue: nil)
     }
 
     private var carbLimitExceeded: Bool {
@@ -84,10 +92,10 @@ struct CarbEntryEditorView: View {
 
             Button(
                 action: {
-                    let treatmentObjectID = carbEntry.objectID
+                    guard let entryToEdit = entryToEdit else { return }
 
                     state.updateEntry(
-                        treatmentObjectID,
+                        entryToEdit,
                         newCarbs: editedCarbs,
                         newFat: editedFat,
                         newProtein: editedProtein,
@@ -173,26 +181,34 @@ struct CarbEntryEditorView: View {
             }
         }
         .task {
-            // TODO: do we still need this, or is grabbing the entire "hold-it-all" entry enough?
+            /*
+             User taps on a FPU entry in the DataTable list. There are two cases:
+             - the User has entered this FPU entry WITH carbs
+             - the User has entered this FPU entry WITHOUT carbs
+             In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+             In the second case, we need to load the zero-carb entry that actualy holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+             */
             if carbEntry.isFPU {
-                if let originalEntryID = await state.getZeroCarbNonFPUEntry(carbEntry.objectID) {
-                    let context = CoreDataStack.shared.persistentContainer.viewContext
-
-                    await context.perform {
-                        do {
-                            if let originalEntry = try context.existingObject(with: originalEntryID) as? CarbEntryStored {
-                                editedFat = Decimal(originalEntry.fat)
-                                editedProtein = Decimal(originalEntry.protein)
-                                editedNote = originalEntry.note ?? ""
-                            }
-                        } catch {
-                            debugPrint(
-                                "\(DebuggingIdentifiers.failed) Failed to fetch original entry: \(error.localizedDescription)"
-                            )
-                        }
-                    }
-                } else {
-                    debugPrint("\(DebuggingIdentifiers.failed) No original entry ID found")
+                if let result = await state.handleFPUEntry(carbEntry.objectID) {
+                    editedCarbs = result.entryValues?.carbs ?? 0
+                    editedFat = result.entryValues?.fat ?? 0
+                    editedProtein = result.entryValues?.protein ?? 0
+                    editedNote = result.entryValues?.note ?? ""
+                    entryToEdit = result.entryID
+                }
+                /*
+                 User taps on a carb entry in the DataTable list. There are again two cases which don't need explicit handling:
+                 - the User has only entered carbs
+                 - the User has entered carbs with FPU
+                 In both cases, we need to simply load the carb entry that holds all the necessary values for us. This is the entry we want to edit.
+                 */
+            } else {
+                if let values = await state.loadEntryValues(from: carbEntry.objectID) {
+                    editedCarbs = values.carbs
+                    editedFat = values.fat
+                    editedProtein = values.protein
+                    editedNote = values.note
+                    entryToEdit = carbEntry.objectID
                 }
             }
         }