|
|
@@ -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
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|