diff --git a/KisaniCal/Models/TaskItem.swift b/KisaniCal/Models/TaskItem.swift index 5f173ad..dd2c6a9 100644 --- a/KisaniCal/Models/TaskItem.swift +++ b/KisaniCal/Models/TaskItem.swift @@ -332,52 +332,53 @@ class TaskViewModel: ObservableObject { return nil } - // MARK: - Eisenhower Matrix planning window + // MARK: - Eisenhower Matrix (classic: you set importance, time sets urgency) // - // The Matrix is a priority view, not a date-filtered list, so far-future - // recurring occurrences must be hidden deliberately. A recurring task surfaces - // exactly one "current actionable occurrence" and only within its planning - // window; completing it rolls forward and it leaves the quadrant until the next - // occurrence's window opens. Completed occurrences never move to another quadrant. + // Importance is intrinsic — taken from the task's assigned quadrant's row + // (Q1/Q2 = important, Q3/Q4 = unimportant) and only changes when you move it. + // Urgency is derived from the deadline: a task is urgent when its (next active) + // due date is within `urgentWindowDays`. The displayed quadrant = importance × urgency, + // so important tasks slide Q2→Q1 as they near, unimportant slide Q4→Q3, and + // recurring tasks roll forward (a completed occurrence is replaced by the next). - private func matrixLeadDays(_ label: String?) -> Int { - switch label { - case "Daily", "Every Weekday": return 0 // today only - case "Weekly": return 6 // the days leading into the due weekday - case "Monthly": return 7 - case "Yearly": return 30 - default: return 0 + static let urgentWindowDays = 7 + + private func isImportantQuadrant(_ q: Quadrant) -> Bool { q == .urgent || q == .schedule } + + /// The due date that drives urgency now: a recurring task's next active + /// occurrence, or a one-off task's own due date. + private func effectiveDue(_ t: TaskItem, on today: Date) -> Date? { + recurs(t) ? nextActiveOccurrence(t, from: today) : t.dueDate + } + + private func isUrgent(_ t: TaskItem, on today: Date) -> Bool { + guard let due = effectiveDue(t, on: today) else { return false } // undated → never urgent + let days = Calendar.current.dateComponents([.day], + from: Calendar.current.startOfDay(for: today), + to: Calendar.current.startOfDay(for: due)).day ?? 0 + return days <= Self.urgentWindowDays // due soon / overdue + } + + /// Where a task should appear in the Matrix right now (importance × urgency). + func displayQuadrant(_ t: TaskItem, on today: Date = Date()) -> Quadrant { + switch (isImportantQuadrant(t.quadrant), isUrgent(t, on: today)) { + case (true, true): return .urgent + case (true, false): return .schedule + case (false, true): return .delegate_ + case (false, false): return .eliminate } } - /// The single occurrence a recurring task should show in the Matrix now, or nil - /// if it's outside its planning window or already done for the current period. - /// Deadlines (monthly/yearly) also keep surfacing a missed past occurrence until - /// it's completed; habits (daily/weekly) only consider the current window. - func matrixActiveOccurrence(_ t: TaskItem, on today: Date = Date()) -> Date? { - guard recurs(t) else { return nil } - let cal = Calendar.current - let t0 = cal.startOfDay(for: today) - let lead = matrixLeadDays(t.recurrenceLabel) - let isDeadline = (t.recurrenceLabel == "Monthly" || t.recurrenceLabel == "Yearly") - let back = isDeadline ? 400 : 0 - guard let windowEnd = cal.date(byAdding: .day, value: lead, to: t0) else { return nil } - var cur = cal.date(byAdding: .day, value: -back, to: t0) ?? t0 - while cur <= windowEnd { - if isOccurrence(t, on: cur) && !occurrenceComplete(t, on: cur) { return cur } - cur = cal.date(byAdding: .day, value: 1, to: cur) ?? cur - } - return nil - } - - /// Matrix rows for a quadrant: non-recurring tasks as-is, plus each recurring - /// task's current actionable occurrence (hidden once done for its period). + /// Matrix rows for a quadrant, grouped by the *displayed* (computed) quadrant. + /// Recurring tasks contribute their single next-active occurrence. func matrixTasks(for quadrant: Quadrant, on today: Date = Date()) -> [TaskItem] { var out: [TaskItem] = [] - for t in tasks where t.quadrant == quadrant { + for t in tasks { if recurs(t) { - if let occ = matrixActiveOccurrence(t, on: today) { out.append(occurrenceCopy(t, on: occ)) } - } else { + guard let occ = nextActiveOccurrence(t, from: today) else { continue } + let copy = occurrenceCopy(t, on: occ) + if displayQuadrant(copy, on: today) == quadrant { out.append(copy) } + } else if displayQuadrant(t, on: today) == quadrant { out.append(t) } } diff --git a/KisaniCal/Views/MatrixView.swift b/KisaniCal/Views/MatrixView.swift index 28eadcc..d9f0bad 100644 --- a/KisaniCal/Views/MatrixView.swift +++ b/KisaniCal/Views/MatrixView.swift @@ -56,7 +56,7 @@ struct MatrixView: View { quadrant: .urgent, roman: "I", label: "Urgent & Important", badgeBg: AppColors.accentSoft, badgeColor: AppColors.accent, - tasks: taskVM.tasks(for: .urgent).filter { !hideCompleted || !$0.isComplete }, + tasks: taskVM.matrixTasks(for: .urgent).filter { !hideCompleted || !$0.isComplete }, isDropTarget: dropTarget == .urgent, onToggle: { taskVM.toggle($0) }, onDrop: { id in withAnimation(KisaniSpring.snappy) { taskVM.moveToQuadrant(taskID: id, quadrant: .urgent); dropTarget = nil } }, @@ -74,7 +74,7 @@ struct MatrixView: View { quadrant: .schedule, roman: "II", label: "Not Urgent & Important", badgeBg: AppColors.yellowSoft, badgeColor: AppColors.yellow, - tasks: taskVM.tasks(for: .schedule).filter { !hideCompleted || !$0.isComplete }, + tasks: taskVM.matrixTasks(for: .schedule).filter { !hideCompleted || !$0.isComplete }, isDropTarget: dropTarget == .schedule, onToggle: { taskVM.toggle($0) }, onDrop: { id in withAnimation(KisaniSpring.snappy) { taskVM.moveToQuadrant(taskID: id, quadrant: .schedule); dropTarget = nil } }, @@ -96,7 +96,7 @@ struct MatrixView: View { quadrant: .delegate_, roman: "III", label: "Urgent & Unimportant", badgeBg: AppColors.blueSoft, badgeColor: AppColors.blue, - tasks: taskVM.tasks(for: .delegate_).filter { !hideCompleted || !$0.isComplete }, + tasks: taskVM.matrixTasks(for: .delegate_).filter { !hideCompleted || !$0.isComplete }, isDropTarget: dropTarget == .delegate_, onToggle: { taskVM.toggle($0) }, onDrop: { id in withAnimation(KisaniSpring.snappy) { taskVM.moveToQuadrant(taskID: id, quadrant: .delegate_); dropTarget = nil } }, @@ -114,7 +114,7 @@ struct MatrixView: View { quadrant: .eliminate, roman: "IV", label: "Not Urgent & Unimportant", badgeBg: AppColors.greenSoft, badgeColor: AppColors.green, - tasks: taskVM.tasks(for: .eliminate).filter { !hideCompleted || !$0.isComplete }, + tasks: taskVM.matrixTasks(for: .eliminate).filter { !hideCompleted || !$0.isComplete }, isDropTarget: dropTarget == .eliminate, onToggle: { taskVM.toggle($0) }, onDrop: { id in withAnimation(KisaniSpring.snappy) { taskVM.moveToQuadrant(taskID: id, quadrant: .eliminate); dropTarget = nil } }, @@ -408,21 +408,23 @@ struct QuadrantDetailView: View { @State private var showAllCompleted = false private let completedLimit = 5 + // Grouped by the *computed* quadrant (importance × deadline urgency); recurring + // tasks contribute their single next-active occurrence. private var overdueTasks: [TaskItem] { let startOfDay = Calendar.current.startOfDay(for: Date()) - return taskVM.tasks - .filter { $0.quadrant == quadrant && !$0.isComplete && ($0.dueDate ?? .distantFuture) < startOfDay } + return taskVM.matrixTasks(for: quadrant) + .filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) < startOfDay } .sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) } } private var laterTasks: [TaskItem] { let startOfDay = Calendar.current.startOfDay(for: Date()) - return taskVM.tasks - .filter { $0.quadrant == quadrant && !$0.isComplete && ($0.dueDate ?? .distantFuture) >= startOfDay } + return taskVM.matrixTasks(for: quadrant) + .filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= startOfDay } .sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) } } private var completedTasks: [TaskItem] { - taskVM.tasks - .filter { $0.quadrant == quadrant && $0.isComplete } + taskVM.matrixTasks(for: quadrant) + .filter { $0.isComplete } .sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) } } private var allEmpty: Bool { overdueTasks.isEmpty && laterTasks.isEmpty && completedTasks.isEmpty }