Matrix: TickTick-style Priority×deadline view + Progress Dots widget (KC-21)
Matrix (KC-21): the Eisenhower Matrix is now a computed view of tasks. Importance = Priority (High = top row), urgency = deadline proximity, so tasks auto-place into Q1–Q4. Added a persisted `matrixOverride` so a manual drag pins the task and syncs its Priority to that row (top → High, moving down demotes High → Medium) and never gets forced back; editing Priority, due date, or the editor clears the override to re-place live. New tasks get KisaniCal priority defaults (birthday/domain/annual/exam/subscription → High, workout → Medium). AddTaskSheet drops the manual quadrant picker for a Priority menu (pick Priority + Date only). Also includes in-progress Progress Dots widget work (day/week/month/custom event modes, App-Group anchor, recurrence-aware spans). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,9 @@ struct TaskItem: Identifiable, Codable, Equatable {
|
||||
// Recurrence: optional so existing saved tasks decode safely (missing key → nil).
|
||||
var recurrenceEnd: Date? = nil // "repeat until" (nil = open-ended)
|
||||
var completedOccurrences: [String]? = nil // "yyyy-MM-dd" of completed occurrences
|
||||
// Matrix: a manual drag pins the task to a quadrant (overrides auto-placement).
|
||||
// nil = auto-place from Priority × deadline. Optional so old saves decode safely.
|
||||
var matrixOverride: Quadrant? = nil
|
||||
}
|
||||
|
||||
enum Priority: String, Codable, CaseIterable, Identifiable {
|
||||
@@ -332,18 +335,21 @@ class TaskViewModel: ObservableObject {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Eisenhower Matrix (classic: you set importance, time sets urgency)
|
||||
// MARK: - Eisenhower Matrix (TickTick-style: Priority × deadline, with manual override)
|
||||
//
|
||||
// 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).
|
||||
// The Matrix is a VIEW of tasks, not a separate system. A task is auto-placed:
|
||||
// • Importance (top row) = Priority is High; Medium/Low/None = bottom row.
|
||||
// • Urgency (left column) = its (next active) due date is today/overdue/within
|
||||
// `urgentWindowDays`; undated or far-out = right column.
|
||||
// So Q1 = High + urgent, Q2 = High + not urgent, Q3 = not-High + urgent, Q4 = the rest.
|
||||
// A manual drag (`moveToQuadrant`) pins the task via `matrixOverride` and syncs its
|
||||
// Priority to the target row — the move sticks and is never forced back. Editing the
|
||||
// Priority or due date in the task view clears the override so it re-places live.
|
||||
// Recurring tasks roll forward (a completed occurrence is replaced by the next).
|
||||
|
||||
static let urgentWindowDays = 7
|
||||
|
||||
private func isImportantQuadrant(_ q: Quadrant) -> Bool { q == .urgent || q == .schedule }
|
||||
private func isImportant(_ t: TaskItem) -> Bool { t.priority == .high }
|
||||
|
||||
/// The due date that drives urgency now: a recurring task's next active
|
||||
/// occurrence, or a one-off task's own due date.
|
||||
@@ -359,9 +365,9 @@ class TaskViewModel: ObservableObject {
|
||||
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)) {
|
||||
/// Auto-placement from Priority (importance) × deadline (urgency).
|
||||
private func autoQuadrant(_ t: TaskItem, on today: Date) -> Quadrant {
|
||||
switch (isImportant(t), isUrgent(t, on: today)) {
|
||||
case (true, true): return .urgent
|
||||
case (true, false): return .schedule
|
||||
case (false, true): return .delegate_
|
||||
@@ -369,6 +375,11 @@ class TaskViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a task appears in the Matrix now: a manual override wins, else auto-placement.
|
||||
func displayQuadrant(_ t: TaskItem, on today: Date = Date()) -> Quadrant {
|
||||
t.matrixOverride ?? autoQuadrant(t, on: today)
|
||||
}
|
||||
|
||||
/// 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] {
|
||||
@@ -409,6 +420,7 @@ class TaskViewModel: ObservableObject {
|
||||
func setDate(_ task: TaskItem, date: Date?) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
tasks[idx].dueDate = date
|
||||
tasks[idx].matrixOverride = nil // deadline changed → re-place from urgency
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -421,14 +433,40 @@ class TaskViewModel: ObservableObject {
|
||||
func setPriority(_ task: TaskItem, priority: Priority) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
tasks[idx].priority = priority
|
||||
tasks[idx].matrixOverride = nil // priority changed → re-place from importance
|
||||
save()
|
||||
}
|
||||
|
||||
/// Manual drag/move in the Matrix. Pins the task to the target quadrant and syncs
|
||||
/// its Priority to that row — top row (Q1/Q2) becomes High; moving down out of the
|
||||
/// top demotes High→Medium so it reads as unimportant. TickTick-style: it sticks.
|
||||
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
|
||||
tasks[idx].quadrant = quadrant
|
||||
tasks[idx].matrixOverride = quadrant
|
||||
tasks[idx].quadrant = quadrant // keep stored quadrant in sync (widget accent)
|
||||
let importantRow = (quadrant == .urgent || quadrant == .schedule)
|
||||
if importantRow {
|
||||
tasks[idx].priority = .high
|
||||
} else if tasks[idx].priority == .high {
|
||||
tasks[idx].priority = .medium
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
/// KisaniCal priority defaults — high-stakes events start High, workouts Medium.
|
||||
/// Applied only when the user didn't pick a priority (== .none) at creation.
|
||||
private func defaultPriority(category: TaskCategory, title: String) -> Priority {
|
||||
switch category {
|
||||
case .birthday, .domain, .annual: return .high
|
||||
default: break
|
||||
}
|
||||
let t = title.lowercased()
|
||||
if t.contains("birthday") || t.contains("exam") || t.contains("subscription")
|
||||
|| t.contains("renew") || t.contains("domain") || t.contains("expir") { return .high }
|
||||
if t.contains("workout") || t.contains("gym") || t.contains("exercise") { return .medium }
|
||||
return .none
|
||||
}
|
||||
|
||||
func addTask(title: String, dueDate: Date?, hasTime: Bool = false,
|
||||
quadrant: Quadrant = .urgent, category: TaskCategory = .reminder,
|
||||
taskColor: TaskColor = .orange, isRecurring: Bool = false,
|
||||
@@ -436,12 +474,14 @@ class TaskViewModel: ObservableObject {
|
||||
endDate: Date? = nil, isAllDay: Bool = false,
|
||||
priority: Priority = .none, reminderDate: Date? = nil,
|
||||
constantReminder: Bool = false, recurrenceEnd: Date? = nil) {
|
||||
let resolvedPriority = priority == .none
|
||||
? defaultPriority(category: category, title: title) : priority
|
||||
var item = TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
|
||||
quadrant: quadrant, category: category,
|
||||
taskColor: taskColor, isRecurring: isRecurring,
|
||||
recurrenceLabel: recurrenceLabel,
|
||||
endDate: endDate, isAllDay: isAllDay,
|
||||
priority: priority, reminderDate: reminderDate,
|
||||
priority: resolvedPriority, reminderDate: reminderDate,
|
||||
constantReminder: constantReminder)
|
||||
item.recurrenceEnd = recurrenceEnd
|
||||
tasks.append(item)
|
||||
@@ -463,6 +503,7 @@ class TaskViewModel: ObservableObject {
|
||||
tasks[idx].reminderDate = reminderDate
|
||||
tasks[idx].constantReminder = constantReminder
|
||||
tasks[idx].recurrenceEnd = recurrenceEnd
|
||||
tasks[idx].matrixOverride = nil // editor saved a new deadline → re-place
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -488,4 +529,3 @@ class TaskViewModel: ObservableObject {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user