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

+ 39 - 0
FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift

@@ -11,6 +11,8 @@ protocol TempTargetsStorage {
     func storeTempTarget(tempTarget: TempTarget) async
     func saveTempTargetsToStorage(_ targets: [TempTarget])
     func fetchForTempTargetPresets() async -> [NSManagedObjectID]
+    func fetchScheduledTempTargets() async -> [NSManagedObjectID]
+    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
@@ -67,6 +69,43 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         }
     }
 
+    func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
+        let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
+
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: scheduledTempTargets,
+            key: "date",
+            ascending: false
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await backgroundContext.perform {
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
+
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: predicate,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await backgroundContext.perform {
+            fetchedResults.map(\.objectID)
+        }
+    }
+
     func storeTempTarget(tempTarget: TempTarget) async {
         var presetCount = -1
         if tempTarget.isPreset == true {

+ 99 - 8
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -53,6 +53,7 @@ extension OverrideConfig {
         var date = Date()
         var newPresetName = ""
         var tempTargetPresets: [TempTargetStored] = []
+        var scheduledTempTargets: [TempTargetStored] = []
         var percentage: Double = 100
         var maxValue: Decimal = 1.2
         var minValue: Decimal = 0.15
@@ -644,31 +645,121 @@ extension OverrideConfig.StateModel {
         }
     }
 
-    // Fill the array of the Temp Target Presets to display them in the UI
-    private func setupTempTargetPresetsArray() {
+    private func setupTempTargets(
+        fetchFunction: @escaping () async -> [NSManagedObjectID],
+        updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
+    ) {
         Task {
-            let ids = await tempTargetStorage.fetchForTempTargetPresets()
-            await updateTempTargetPresetsArray(with: ids)
+            let ids = await fetchFunction()
+            let tempTargetObjects = await fetchTempTargetObjects(for: ids)
+            await updateFunction(tempTargetObjects)
         }
     }
 
-    @MainActor private func updateTempTargetPresetsArray(with IDs: [NSManagedObjectID]) async {
+    @MainActor private func fetchTempTargetObjects(for IDs: [NSManagedObjectID]) async -> [TempTargetStored] {
         do {
-            let tempTargetObjects = try IDs.compactMap { id in
+            return try IDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? TempTargetStored
             }
-            tempTargetPresets = tempTargetObjects
         } catch {
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Temp Targets as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
             )
+            return []
         }
     }
 
+    private func setupTempTargetPresetsArray() {
+        setupTempTargets(
+            fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
+            updateFunction: { tempTargets in
+                self.tempTargetPresets = tempTargets
+            }
+        )
+    }
+
+    private func setupScheduledTempTargetsArray() {
+        setupTempTargets(
+            fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
+            updateFunction: { tempTargets in
+                self.scheduledTempTargets = tempTargets
+            }
+        )
+    }
+
     func saveTempTargetToStorage(tempTargets: [TempTarget]) {
         tempTargetStorage.saveTempTargetsToStorage(tempTargets)
     }
 
+    func invokeSaveOfCustomTempTargets() async {
+        if date > Date() {
+            await saveScheduledTempTarget()
+        } else {
+            await saveCustomTempTarget()
+        }
+    }
+
+    // Save scheduled Preset to Core Data
+    func saveScheduledTempTarget() async {
+        guard date > Date() else { return }
+
+        let tempTarget = TempTarget(
+            name: tempTargetName,
+            createdAt: date,
+            targetTop: tempTargetTarget,
+            targetBottom: tempTargetTarget,
+            duration: tempTargetDuration,
+            enteredBy: TempTarget.manual,
+            reason: TempTarget.custom,
+            isPreset: false,
+            enabled: false,
+            halfBasalTarget: halfBasalTarget
+        )
+
+        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+
+        // Update Scheduled Temp Targets Array
+        setupScheduledTempTargetsArray()
+
+        // If the scheduled date equals Date() enable the Preset
+        Task {
+            await waitUntilDate(date)
+
+            await enableScheduledTempTarget(for: date)
+        }
+    }
+
+    private func enableScheduledTempTarget(for date: Date) async {
+        let ids = await tempTargetStorage.fetchScheduledTempTarget(for: date)
+
+        guard let firstID = ids.first else {
+            debugPrint("No Temp Target found for the specified date.")
+            return
+        }
+
+        await MainActor.run {
+            do {
+                if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
+                    tempTarget.enabled = true
+                    try viewContext.save()
+                }
+            } catch {
+                debugPrint("Failed to enable the Temp Target for the specified date: \(error.localizedDescription)")
+            }
+        }
+
+        // Refresh the list of scheduled Temp Targets
+        setupScheduledTempTargetsArray()
+    }
+
+    private func waitUntilDate(_ targetDate: Date) async {
+        while Date() < targetDate {
+            let timeInterval = targetDate.timeIntervalSince(Date())
+            let sleepDuration = min(timeInterval, 60.0) // check every 60s
+            try? await Task.sleep(nanoseconds: UInt64(sleepDuration * 1_000_000_000))
+        }
+    }
+
     // Creates and enacts a non Preset Temp Target
     func saveCustomTempTarget() async {
         // First disable all active TempTargets
@@ -676,7 +767,7 @@ extension OverrideConfig.StateModel {
 
         let tempTarget = TempTarget(
             name: tempTargetName,
-            createdAt: Date(),
+            createdAt: date,
             targetTop: tempTargetTarget,
             targetBottom: tempTargetTarget,
             duration: tempTargetDuration,

+ 219 - 77
FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -203,23 +203,20 @@ extension OverrideConfig {
             } else {
                 defaultText
             }
-//            if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
-//                cancelAdjustmentButton
-//            }
         }
 
         @ViewBuilder func tempTargets() -> some View {
             if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
                 currentActiveAdjustment
             }
+            if state.scheduledTempTargets.isNotEmpty {
+                scheduledTempTargets
+            }
             if state.tempTargetPresets.isNotEmpty {
                 tempTargetPresets
             } else {
                 defaultText
             }
-//            if state.tempTargetPresets.isNotEmpty || state.currentActiveTempTarget != nil {
-//                cancelAdjustmentButton
-//            }
         }
 
         private var defaultText: some View {
@@ -313,67 +310,36 @@ extension OverrideConfig {
             }
         }
 
+        private var scheduledTempTargets: some View {
+            Section {
+                ForEach(state.scheduledTempTargets) { tt in
+                    tempTargetView(for: tt)
+                }
+                .listRowBackground(Color.chart)
+            } header: {
+                Text("Scheduled Temp Targets")
+            }
+        }
+
         private var tempTargetPresets: some View {
             Section {
                 ForEach(state.tempTargetPresets) { preset in
-                    tempTargetView(for: preset)
-                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                            Button(role: .none) {
-                                Task {
-                                    selectedTempTarget = preset
-                                    isConfirmDeletePresented = true
-                                }
-                            } label: {
-                                Label("Delete", systemImage: "trash")
-                                    .tint(.red)
-                            }
-                            Button(action: {
-                                // Set the selected Temp Target to the chosen Preset and pass it to the Edit Sheet
-                                selectedTempTarget = preset
-                                state.showTempTargetEditSheet = true
-                            }, label: {
-                                Label("Edit", systemImage: "pencil")
-                                    .tint(.blue)
-                            })
-                        }
+                    tempTargetView(for: preset, showCheckmark: showCheckmark) {
+                        enactTempTargetPreset(preset)
+                    }
+                    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                        swipeActions(for: preset)
+                    }
                 }
                 .onMove(perform: state.reorderTempTargets)
                 .confirmationDialog(
-                    "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?",
+                    deleteConfirmationTitle,
                     isPresented: $isConfirmDeletePresented,
                     titleVisibility: .visible
                 ) {
-                    if let itemToDelete = selectedTempTarget {
-                        Button(
-                            state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
-                            role: .destructive
-                        ) {
-                            if state.currentActiveTempTarget == selectedTempTarget {
-                                Task {
-                                    // Save cancelled Temp Target in Temp Target run Entity
-                                    await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-                                }
-                            }
-                            // Perform the stop action
-                            Task {
-                                await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
-                            }
-                            // Reset the selected item after deletion
-                            selectedTempTarget = nil
-                        }
-                    }
-                    Button("Cancel", role: .cancel) {
-                        // Dismiss the dialog without action
-                        selectedTempTarget = nil
-                    }
+                    deleteConfirmationButtons()
                 } message: {
-                    if state.currentActiveTempTarget == selectedTempTarget {
-                        Text(
-                            state
-                                .currentActiveTempTarget == selectedTempTarget ?
-                                "This Temp Target preset is currently running. Deleting will stop it." : ""
-                        )
-                    }
+                    deleteConfirmationMessage
                 }
                 .listRowBackground(Color.chart)
             } header: {
@@ -386,6 +352,75 @@ extension OverrideConfig {
             }
         }
 
+        private func enactTempTargetPreset(_ preset: TempTargetStored) {
+            Task {
+                let objectID = preset.objectID
+                await state.enactTempTargetPreset(withID: objectID)
+                selectedTempTargetPresetID = preset.id?.uuidString
+                showCheckmark.toggle()
+
+                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                    showCheckmark = false
+                }
+            }
+        }
+
+        private func swipeActions(for preset: TempTargetStored) -> some View {
+            Group {
+                Button(role: .destructive) {
+                    Task {
+                        selectedTempTarget = preset
+                        isConfirmDeletePresented = true
+                    }
+                } label: {
+                    Label("Delete", systemImage: "trash")
+                        .tint(.red)
+                }
+                Button(action: {
+                    selectedTempTarget = preset
+                    state.showTempTargetEditSheet = true
+                }, label: {
+                    Label("Edit", systemImage: "pencil")
+                        .tint(.blue)
+                })
+            }
+        }
+
+        private var deleteConfirmationTitle: String {
+            "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
+        }
+
+        private func deleteConfirmationButtons() -> some View {
+            Group {
+                if let itemToDelete = selectedTempTarget {
+                    Button(
+                        state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
+                        role: .destructive
+                    ) {
+                        if state.currentActiveTempTarget == selectedTempTarget {
+                            Task {
+                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                            }
+                        }
+                        Task {
+                            await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
+                        }
+                        selectedTempTarget = nil
+                    }
+                }
+                Button("Cancel", role: .cancel) {
+                    selectedTempTarget = nil
+                }
+            }
+        }
+
+        private var deleteConfirmationMessage: Text? {
+            if state.currentActiveTempTarget == selectedTempTarget {
+                return Text("This Temp Target preset is currently running. Deleting will stop it.")
+            }
+            return nil
+        }
+
         private var currentActiveAdjustment: some View {
             switch state.selectedTab {
             case .overrides:
@@ -528,7 +563,11 @@ extension OverrideConfig {
             }
         }
 
-        private func tempTargetView(for preset: TempTargetStored) -> some View {
+        private func tempTargetView(
+            for preset: TempTargetStored,
+            showCheckmark: Bool = false,
+            onTap: (() -> Void)? = nil
+        ) -> some View {
             let target = preset.target ?? 100
             let presetTarget = Decimal(target as! Double.RawValue)
             let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
@@ -540,7 +579,7 @@ extension OverrideConfig {
                 state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
             )
 
-            return ZStack(alignment: .trailing, content: {
+            return ZStack(alignment: .trailing) {
                 HStack {
                     VStack {
                         HStack {
@@ -548,7 +587,7 @@ extension OverrideConfig {
                             Spacer()
                         }
                         HStack(spacing: 2) {
-                            Text(formattedGlucose(glucose: target as! Decimal))
+                            Text(formattedGlucose(glucose: target as Decimal))
                                 .foregroundColor(.secondary)
                                 .font(.caption)
                             Text("for")
@@ -560,41 +599,144 @@ extension OverrideConfig {
                             Text("min")
                                 .foregroundColor(.secondary)
                                 .font(.caption)
-                            if state.isAdjustSensEnabled(usingTarget: presetTarget) { Text(", \(percentage)%")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
+                            if state.isAdjustSensEnabled(usingTarget: presetTarget) {
+                                Text(", \(percentage)%")
+                                    .foregroundColor(.secondary)
+                                    .font(.caption)
                             }
                             Spacer()
-                        }.padding(.top, 2)
+                        }
+                        .padding(.top, 2)
                     }
                     .contentShape(Rectangle())
                     .onTapGesture {
-                        Task {
-                            let objectID = preset.objectID
-                            await state.enactTempTargetPreset(withID: objectID)
-                            selectedTempTargetPresetID = preset.id?.uuidString
-                            showCheckmark.toggle()
-
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                                showCheckmark = false
-                            }
-                        }
+                        onTap?()
                     }
                 }
                 if showCheckmark && isSelected {
-                    // show checkmark to indicate if the preset was actually pressed
                     Image(systemName: "checkmark.circle.fill")
                         .imageScale(.large)
                         .fontWeight(.bold)
                         .foregroundStyle(Color.green)
-                } else {
+                } else if onTap != nil {
                     Image(systemName: "line.3.horizontal")
                         .imageScale(.medium)
                         .foregroundStyle(.secondary)
                 }
-            })
+            }
         }
 
+//        private func tempTargetView(for preset: TempTargetStored) -> some View {
+//            let target = preset.target ?? 100
+//            let presetTarget = Decimal(target as! Double.RawValue)
+//            let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
+//            let presetHalfBasalTarget = Decimal(
+//                preset.halfBasalTarget as? Double
+//                    .RawValue ?? Double(state.settingHalfBasalTarget)
+//            )
+//            let percentage = Int(
+//                state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
+//            )
+//
+//            return ZStack(alignment: .trailing, content: {
+//                HStack {
+//                    VStack {
+//                        HStack {
+//                            Text(preset.name ?? "")
+//                            Spacer()
+//                        }
+//                        HStack(spacing: 2) {
+//                            Text(formattedGlucose(glucose: target as Decimal))
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            Text("for")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            Text("min")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            if state.isAdjustSensEnabled(usingTarget: presetTarget) { Text(", \(percentage)%")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            }
+//                            Spacer()
+//                        }.padding(.top, 2)
+//                    }
+//                    .contentShape(Rectangle())
+//                    .onTapGesture {
+//                        Task {
+//                            let objectID = preset.objectID
+//                            await state.enactTempTargetPreset(withID: objectID)
+//                            selectedTempTargetPresetID = preset.id?.uuidString
+//                            showCheckmark.toggle()
+//
+//                            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+//                                showCheckmark = false
+//                            }
+//                        }
+//                    }
+//                }
+//                if showCheckmark && isSelected {
+//                    // show checkmark to indicate if the preset was actually pressed
+//                    Image(systemName: "checkmark.circle.fill")
+//                        .imageScale(.large)
+//                        .fontWeight(.bold)
+//                        .foregroundStyle(Color.green)
+//                } else {
+//                    Image(systemName: "line.3.horizontal")
+//                        .imageScale(.medium)
+//                        .foregroundStyle(.secondary)
+//                }
+//            })
+//        }
+//
+//        private func scheduledTempTargetView(for preset: TempTargetStored) -> some View {
+//            let target = preset.target ?? 100
+//            let presetTarget = Decimal(target as! Double.RawValue)
+//            let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
+//            let presetHalfBasalTarget = Decimal(
+//                preset.halfBasalTarget as? Double
+//                    .RawValue ?? Double(state.settingHalfBasalTarget)
+//            )
+//            let percentage = Int(
+//                state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
+//            )
+//
+//            return ZStack(alignment: .trailing, content: {
+//                HStack {
+//                    VStack {
+//                        HStack {
+//                            Text(preset.name ?? "")
+//                            Spacer()
+//                        }
+//                        HStack(spacing: 2) {
+//                            Text(formattedGlucose(glucose: target as Decimal))
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            Text("for")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            Text("min")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            if state.isAdjustSensEnabled(usingTarget: presetTarget) { Text(", \(percentage)%")
+//                                .foregroundColor(.secondary)
+//                                .font(.caption)
+//                            }
+//                            Spacer()
+//                        }.padding(.top, 2)
+//                    }
+//                    .contentShape(Rectangle())
+//                }
+//            })
+//        }
+
         private var overrideLabelDivider: some View {
             Divider()
                 .frame(width: 1, height: 20)

+ 2 - 2
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -297,8 +297,8 @@ struct AddTempTargetForm: View {
                             if noNameSpecified { state.tempTargetName = "Custom Target" }
                             didPressSave.toggle()
                             state.isTempTargetEnabled.toggle()
+                            await state.invokeSaveOfCustomTempTargets()
                             dismiss()
-                            await state.saveCustomTempTarget()
                         }
                     }, label: {
                         Text("Start Temp Target")
@@ -314,8 +314,8 @@ struct AddTempTargetForm: View {
                     Task {
                         if noNameSpecified { state.tempTargetName = "Custom Target" }
                         didPressSave.toggle()
-                        dismiss()
                         await state.saveTempTargetPreset()
+                        dismiss()
                     }
                 }, label: {
                     Text("Save as Preset")

+ 39 - 2
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift

@@ -60,6 +60,13 @@ struct EditTempTargetForm: View {
             )
     }
 
+    private var dateFormatter: DateFormatter {
+        let f = DateFormatter()
+        f.dateStyle = .short
+        f.timeStyle = .short
+        return f
+    }
+
     var body: some View {
         NavigationView {
             List {
@@ -87,6 +94,28 @@ struct EditTempTargetForm: View {
         }
     }
 
+    private func calculatedEndDate(from startDate: Date, totalDuration: Decimal) -> Date {
+        let elapsedTime = Date().timeIntervalSince(startDate)
+        let totalDurationSeconds = Int(totalDuration) * 60
+        let remainingTime = max(totalDurationSeconds - Int(elapsedTime), 0)
+        return Date().addingTimeInterval(TimeInterval(remainingTime))
+    }
+
+    private func formattedEndTime(startDate: Date, totalDuration: Decimal) -> String {
+        let endDate = calculatedEndDate(from: startDate, totalDuration: totalDuration)
+        let formatter = DateFormatter()
+
+        if Calendar.current.isDateInToday(endDate) {
+            formatter.dateStyle = .none
+            formatter.timeStyle = .short // show only the time
+        } else {
+            formatter.dateStyle = .short
+            formatter.timeStyle = .short // show Date and time
+        }
+
+        return formatter.string(from: endDate)
+    }
+
     @ViewBuilder private func editTempTarget() -> some View {
         Group {
             Section {
@@ -203,7 +232,7 @@ struct EditTempTargetForm: View {
             }
 
             Section {
-                DatePicker("Date", selection: $date)
+                DatePicker("Start Time", selection: $date)
                     .onChange(of: date) { hasChanges = true }
             }.listRowBackground(Color.chart)
 
@@ -218,7 +247,6 @@ struct EditTempTargetForm: View {
                     .onTapGesture {
                         displayPickerDuration = toggleScrollWheel(displayPickerDuration)
                     }
-//                    .onChange(of: duration) { hasChanges = true }
 
                     if displayPickerDuration {
                         HStack {
@@ -267,6 +295,15 @@ struct EditTempTargetForm: View {
                     }
                 }
             }.listRowBackground(Color.chart)
+
+            if isEnabled {
+                Section {
+                    HStack {
+                        Spacer()
+                        Text("Until \(formattedEndTime(startDate: date, totalDuration: duration))").foregroundStyle(.secondary)
+                    }
+                }.listRowBackground(Color.clear)
+            }
         }
     }