Ver código fonte

edit carb entries + fpu entries and adapt uploading behaviour to remote services dnzxy/Trio-dev#193

polscm32 aka Marvout 1 ano atrás
pai
commit
f1259ce78e

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -330,6 +330,7 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */; };
+		BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
@@ -1035,6 +1036,7 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetSetup.swift; sourceTree = "<group>"; };
+		BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryEditorView.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
@@ -1361,6 +1363,7 @@
 		0EE66DD474AFFD4FD787D5B9 /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
 				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
 			);
 			path = View;
@@ -3822,6 +3825,7 @@
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
+				BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
 				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,

+ 23 - 2
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -46,8 +46,29 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
 
-        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+        // Check for FPU-only entries (fat/protein without carbs)
+        let fpuOnlyEntries = entriesToStore.filter { entry in
+            entry.carbs == 0 && (entry.fat ?? 0 > 0 || entry.protein ?? 0 > 0)
+        }
+
+        // Create additional Carb (non-FPU) entries with fat/protein amounts and carbs == 0
+        for entry in fpuOnlyEntries {
+            let additionalEntry = CarbsEntry(
+                id: entry.id,
+                createdAt: entry.createdAt,
+                actualDate: entry.actualDate,
+                carbs: Decimal(0),
+                fat: entry.fat,
+                protein: entry.protein,
+                note: entry.note,
+                enteredBy: entry.enteredBy,
+                isFPU: false, // it should be a Carb entry
+                fpuID: entry.fpuID
+            )
+            entriesToStore.append(additionalEntry)
+        }
 
+        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
 
@@ -195,7 +216,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
-        guard let entry = entries.last, entry.carbs != 0 else { return }
+        guard let entry = entries.last else { return }
 
         await coredataContext.perform {
             let newItem = CarbEntryStored(context: self.coredataContext)

+ 136 - 0
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -28,6 +28,9 @@ extension DataTable {
 
         var units: GlucoseUnits = .mgdL
 
+        var carbEntryToEdit: CarbEntryStored?
+        var showCarbEntryEditor = false
+
         override func subscribe() {
             units = settingsManager.settings.units
             broadcaster.register(DeterminationObserver.self, observer: self)
@@ -121,6 +124,60 @@ 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"
@@ -246,6 +303,85 @@ extension DataTable {
                 }
             }
         }
+
+        // Function to get the original zero-carb non-FPU entry
+        func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "fpuContext"
+
+            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 }
+
+                    // Fetch the original zero-carb entry (non-FPU) with the same fpuID
+                    let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
+                    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")
+                    ])
+                    request.fetchLimit = 1
+
+                    let originalEntry = try context.fetch(request).first
+                    debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
+                    return originalEntry?.objectID
+
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
+
+        func updateFPUEntry(_ treatmentObjectID: NSManagedObjectID, 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)
+
+                // Create new FPU entry with updated values
+                let newEntry = CarbsEntry(
+                    id: UUID().uuidString,
+                    createdAt: Date(),
+                    actualDate: originalDate, // Use the original entry's date
+                    carbs: Decimal(0),
+                    fat: newFat,
+                    protein: newProtein,
+                    note: newNote,
+                    enteredBy: CarbsEntry.local,
+                    isFPU: true,
+                    fpuID: UUID().uuidString
+                )
+
+                // Store new entry which will create new FPU entries
+                await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+
+                // 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()
+
+                // Wait for all uploads to complete
+                _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
+            }
+        }
     }
 }
 

+ 139 - 0
FreeAPS/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -0,0 +1,139 @@
+//
+//  CarbEntryEditorView.swift
+//  FreeAPS
+//
+//  Created by Marvin Polscheit on 15.01.25.
+//
+import SwiftUI
+
+struct CarbEntryEditorView: View {
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    var state: DataTable.StateModel
+    let carbEntry: CarbEntryStored
+
+    @State private var editedAmount: Decimal
+    @State private var editedFat: Decimal
+    @State private var editedProtein: Decimal
+    @State private var editedNote: String
+    @State private var isFPU: Bool
+
+    init(state: DataTable.StateModel, carbEntry: CarbEntryStored) {
+        self.state = state
+        self.carbEntry = carbEntry
+        _editedAmount = State(initialValue: Decimal(carbEntry.carbs))
+        _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)
+    }
+
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    if isFPU {
+                        HStack {
+                            Text("Fat")
+                            TextFieldWithToolBar(
+                                text: $editedFat,
+                                placeholder: "Enter fat",
+                                numberFormatter: Formatter.integerFormatter
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+
+                        HStack {
+                            Text("Protein")
+                            TextFieldWithToolBar(
+                                text: $editedProtein,
+                                placeholder: "Enter protein",
+                                numberFormatter: Formatter.integerFormatter
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+                    } else {
+                        HStack {
+                            Text("Amount")
+                            TextFieldWithToolBar(
+                                text: $editedAmount,
+                                placeholder: "Enter carbs",
+                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+                    }
+
+                    HStack {
+                        Text("Note")
+                        TextField("Optional note", text: $editedNote)
+                    }
+                }.listRowBackground(Color.chart)
+
+                Section {
+                    HStack {
+                        Spacer()
+                        Button("Save") {
+                            let treatmentObjectID = carbEntry.objectID
+
+                            if isFPU {
+                                state.updateFPUEntry(
+                                    treatmentObjectID,
+                                    newFat: editedFat,
+                                    newProtein: editedProtein,
+                                    newNote: editedNote
+                                )
+                            } else {
+                                state.updateCarbEntry(
+                                    treatmentObjectID,
+                                    newAmount: editedAmount,
+                                    newNote: editedNote
+                                )
+                            }
+                            dismiss()
+                        }
+                        Spacer()
+                    }
+                }
+                .listRowBackground(Color(.systemBlue))
+                .tint(.white)
+            }
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Edit Carbs")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Cancel") {
+                        dismiss()
+                    }
+                }
+            }
+        }
+        .task {
+            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")
+                }
+            }
+        }
+    }
+}

+ 16 - 1
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -40,7 +40,7 @@ extension DataTable {
         @FetchRequest(
             entity: CarbEntryStored.entity(),
             sortDescriptors: [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)],
-            predicate: NSPredicate.predicateForOneDayAgo,
+            predicate: NSPredicate.carbsHistory,
             animation: .bouncy
         ) var carbEntryStored: FetchedResults<CarbEntryStored>
 
@@ -120,6 +120,11 @@ extension DataTable {
                 .sheet(isPresented: $showManualGlucose) {
                     addGlucoseView()
                 }
+                .sheet(isPresented: $state.showCarbEntryEditor) {
+                    if let carbEntry = state.carbEntryToEdit {
+                        CarbEntryEditorView(state: state, carbEntry: carbEntry)
+                    }
+                }
         }
 
         @ViewBuilder func addButton(_ action: @escaping () -> Void) -> some View {
@@ -578,6 +583,16 @@ extension DataTable {
             }
             .swipeActions {
                 Button(
+                    "Edit",
+                    systemImage: "pencil",
+                    role: .none,
+                    action: {
+                        state.carbEntryToEdit = meal
+                        state.showCarbEntryEditor = true
+                    }
+                ).tint(.blue)
+
+                Button(
                     "Delete",
                     systemImage: "trash.fill",
                     role: .none,

+ 2 - 1
FreeAPS/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -567,7 +567,8 @@ extension Treatments {
                 protein: protein,
                 note: note,
                 enteredBy: CarbsEntry.local,
-                isFPU: false, fpuID: UUID().uuidString
+                isFPU: false,
+                fpuID: UUID().uuidString
             )]
             await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
 

+ 4 - 4
Model/Helper/CarbEntryStored+helper.swift

@@ -9,13 +9,13 @@ extension NSPredicate {
 
     static var carbsForChart: NSPredicate {
         let date = Date.oneDayAgo
-        return NSPredicate(format: "isFPU == false AND date >= %@", date as NSDate)
+        return NSPredicate(format: "isFPU == false AND date >= %@ AND carbs > 0", date as NSDate)
     }
 
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
-            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@",
+            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@ AND carbs > 0",
             date as NSDate,
             false as NSNumber,
             false as NSNumber
@@ -25,7 +25,7 @@ extension NSPredicate {
     static var carbsNotYetUploadedToHealth: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
-            format: "date >= %@ AND isUploadedToHealth == %@",
+            format: "date >= %@ AND isUploadedToHealth == %@ AND carbs > 0",
             date as NSDate,
             false as NSNumber
         )
@@ -34,7 +34,7 @@ extension NSPredicate {
     static var carbsNotYetUploadedToTidepool: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
-            format: "date >= %@ AND isUploadedToTidepool == %@",
+            format: "date >= %@ AND isUploadedToTidepool == %@ AND carbs > 0",
             date as NSDate,
             false as NSNumber
         )

+ 5 - 0
Model/Helper/NSPredicates.swift

@@ -66,6 +66,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
 
+    static var carbsHistory: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND carbs > 0", date as NSDate)
+    }
+
     static var predicateForOneHourAgo: NSPredicate {
         let date = Date.oneHourAgo
         return NSPredicate(format: "date >= %@", date as NSDate)