diff --git a/KisaniCal/ContentView.swift b/KisaniCal/ContentView.swift index 2002046..795224a 100644 --- a/KisaniCal/ContentView.swift +++ b/KisaniCal/ContentView.swift @@ -88,6 +88,7 @@ struct ContentView: View { } .onChange(of: scenePhase) { phase in if phase == .active { + taskVM.reload() NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM) } } diff --git a/KisaniCal/Managers/NotificationManager.swift b/KisaniCal/Managers/NotificationManager.swift index 6fd0830..930bc0f 100644 --- a/KisaniCal/Managers/NotificationManager.swift +++ b/KisaniCal/Managers/NotificationManager.swift @@ -11,12 +11,47 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable private let checkInPrefix = "kisani.workout.checkin" private let tasksId = "kisani.tasks.evening" + private static let categoryId = "TASK_REMINDER" + private static let actionMarkId = "MARK_COMPLETE" + override private init() { super.init() center.delegate = self + registerCategories() refreshAuthStatus() } + private func registerCategories() { + let markAction = UNNotificationAction( + identifier: Self.actionMarkId, + title: "Mark Complete", + options: [] + ) + let category = UNNotificationCategory( + identifier: Self.categoryId, + actions: [markAction], + intentIdentifiers: [], + options: .customDismissAction + ) + center.setNotificationCategories([category]) + } + + // MARK: - Complete task directly in UserDefaults (called from background) + private func markTaskComplete(taskId: String) { + let uid = AuthManager.shared.currentUser?.id ?? "shared" + let key = "kisani.tasks.v2.\(uid)" + guard let data = UserDefaults.standard.data(forKey: key), + var tasks = try? JSONDecoder().decode([TaskItem].self, from: data), + let uuid = UUID(uuidString: taskId), + let idx = tasks.firstIndex(where: { $0.id == uuid }) + else { return } + tasks[idx].isComplete = true + tasks[idx].completedAt = Date() + if let encoded = try? JSONEncoder().encode(tasks) { + UserDefaults.standard.set(encoded, forKey: key) + } + } + // MARK: - Permission func requestPermission() { @@ -149,10 +184,11 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable guard fireDate > now else { continue } let content = UNMutableNotificationContent() - content.title = task.title - content.body = self.notifBody(for: task) - content.sound = .default - content.userInfo = ["taskId": task.id.uuidString, "deeplink": "tasks"] + content.title = task.title + content.body = self.notifBody(for: task) + content.sound = .default + content.categoryIdentifier = Self.categoryId + content.userInfo = ["taskId": task.id.uuidString, "deeplink": "tasks"] let trigComps = cal.dateComponents([.year, .month, .day, .hour, .minute], from: fireDate) center.add(UNNotificationRequest( @@ -205,13 +241,24 @@ extension NotificationManager: UNUserNotificationCenterDelegate { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { - let deeplink = response.notification.request.content.userInfo["deeplink"] as? String - Task { @MainActor in - switch deeplink { - case "workout": - ShortcutHandler.shared.handle(QuickAction.workout.rawValue) - default: - ShortcutHandler.shared.handle(QuickAction.today.rawValue) + let info = response.notification.request.content.userInfo + let deeplink = info["deeplink"] as? String + let taskId = info["taskId"] as? String + + switch response.actionIdentifier { + + case Self.actionMarkId: + // Swiped action: mark complete, don't open app + if let taskId { markTaskComplete(taskId: taskId) } + + default: + // Tapped notification body + if let taskId { markTaskComplete(taskId: taskId) } + Task { @MainActor in + switch deeplink { + case "workout": ShortcutHandler.shared.handle(QuickAction.workout.rawValue) + default: ShortcutHandler.shared.handle(QuickAction.today.rawValue) + } } } completionHandler() diff --git a/KisaniCal/Models/TaskItem.swift b/KisaniCal/Models/TaskItem.swift index ac0fc61..081285a 100644 --- a/KisaniCal/Models/TaskItem.swift +++ b/KisaniCal/Models/TaskItem.swift @@ -72,6 +72,13 @@ class TaskViewModel: ObservableObject { } } + func reload() { + guard let data = UserDefaults.standard.data(forKey: persistKey), + let saved = try? JSONDecoder().decode([TaskItem].self, from: data) + else { return } + tasks = saved + } + private func save() { if let data = try? JSONEncoder().encode(tasks) { UserDefaults.standard.set(data, forKey: persistKey)