import Foundation import SwiftUI import WidgetKit // Minimal task model shared between widget and main app via App Group struct WidgetTask: Codable, Identifiable { let id: UUID let title: String let dueDate: Date? let isComplete: Bool let quadrant: String // raw value of Quadrant enum let category: String // Days remaining until due date var daysLeft: Int? { guard let due = dueDate else { return nil } let d = Calendar.current.dateComponents([.day], from: Calendar.current.startOfDay(for: .now), to: due).day ?? 0 return max(0, d) } // Hours remaining (for sub-day precision) var hoursLeft: Int? { guard let due = dueDate else { return nil } let h = Calendar.current.dateComponents([.hour], from: .now, to: due).hour ?? 0 return max(0, h) } var timeLeftLabel: String { guard dueDate != nil else { return "" } if isComplete { return "Done" } let days = daysLeft ?? 0 if days == 0 { let hrs = hoursLeft ?? 0 return hrs <= 0 ? "Due now" : "\(hrs)hr left" } if days < 30 { return "\(days)d left" } let months = days / 30 let remDays = days % 30 if remDays == 0 { return "\(months)mth left" } return "\(months)mth \(remDays)d..." } var accentColor: Color { switch quadrant { case "Urgent + Important": return Color(red: 0.96, green: 0.42, blue: 0.20) case "Schedule It": return Color(red: 0.95, green: 0.78, blue: 0.10) case "Delegate": return Color(red: 0.24, green: 0.58, blue: 0.95) case "Eliminate": return Color(red: 0.20, green: 0.78, blue: 0.50) default: return Color(red: 0.96, green: 0.42, blue: 0.20) } } } // Load tasks from the shared App Group UserDefaults func loadWidgetTasks() -> [WidgetTask] { let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared" let key = "kisani.tasks.v2.\(uid)" guard let data = UserDefaults.kisani.data(forKey: key), let raw = try? JSONDecoder().decode([RawTask].self, from: data) else { return [] } return raw.map { WidgetTask(id: $0.id, title: $0.title, dueDate: $0.dueDate, isComplete: $0.isComplete, quadrant: $0.quadrant, category: $0.category) } } // Partial decode of TaskItem — only the fields widgets need private struct RawTask: Codable { let id: UUID let title: String let dueDate: Date? var isComplete: Bool = false var quadrant: String = "Urgent + Important" var category: String = "reminder" enum CodingKeys: String, CodingKey { case id, title, dueDate, isComplete, quadrant, category } } // Next upcoming task with a due date (for default widget display) func nextDueTask(from tasks: [WidgetTask]) -> WidgetTask? { tasks .filter { !$0.isComplete && $0.dueDate != nil } .sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) } .first } private func activeTasksKey() -> String { let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared" return "kisani.tasks.v2.\(uid)" } // Tasks due today (incomplete first, then by due time) for the interactive list widget. func todayWidgetTasks() -> [WidgetTask] { let cal = Calendar.current let start = cal.startOfDay(for: Date()) let end = cal.date(byAdding: .day, value: 1, to: start)! return loadWidgetTasks() .filter { t in guard let due = t.dueDate else { return false } return due >= start && due < end } .sorted { if $0.isComplete != $1.isComplete { return !$0.isComplete } return ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) } } // Toggle a task's completion from the widget. Uses JSONSerialization so ALL task // fields are preserved on write (the widget only models a subset of TaskItem). func toggleWidgetTaskComplete(id: String) { let key = activeTasksKey() guard let data = UserDefaults.kisani.data(forKey: key), var arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]] else { return } for i in arr.indices where (arr[i]["id"] as? String) == id { let done = (arr[i]["isComplete"] as? Bool) ?? false arr[i]["isComplete"] = !done if !done { // TaskItem uses the default (deferredToDate) Date strategy → seconds since reference date. arr[i]["completedAt"] = Date().timeIntervalSinceReferenceDate } else { arr[i].removeValue(forKey: "completedAt") } } if let out = try? JSONSerialization.data(withJSONObject: arr) { UserDefaults.kisani.set(out, forKey: key) CloudSyncManager_widgetPush(out, key) } WidgetCenter.shared.reloadAllTimelines() } // The widget extension can't see CloudSyncManager; mirror to iCloud KV store directly. private func CloudSyncManager_widgetPush(_ data: Data, _ key: String) { guard data.count < 900_000 else { return } let kv = NSUbiquitousKeyValueStore.default kv.set(data, forKey: key) kv.synchronize() } // Today's task completion: (done, total) for tasks due today. func todayTaskCompletion() -> (done: Int, total: Int) { let cal = Calendar.current let start = cal.startOfDay(for: Date()) let end = cal.date(byAdding: .day, value: 1, to: start)! let tasks = loadWidgetTasks() func dueToday(_ t: WidgetTask) -> Bool { guard let due = t.dueDate else { return false } return due >= start && due < end } let active = tasks.filter { !$0.isComplete && dueToday($0) } let completed = tasks.filter { $0.isComplete && dueToday($0) } return (completed.count, active.count + completed.count) } // Workout streak count func loadWorkoutStreak() -> Int { let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared" let key = "kisani.workout.completedDates.\(uid)" let dates = UserDefaults.kisani.stringArray(forKey: key) ?? [] let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd" let cal = Calendar.current let dateSet = Set(dates) var streak = 0 var check = cal.startOfDay(for: Date()) if !dateSet.contains(fmt.string(from: check)) { check = cal.date(byAdding: .day, value: -1, to: check)! } while dateSet.contains(fmt.string(from: check)) { streak += 1 check = cal.date(byAdding: .day, value: -1, to: check)! } return streak }