Matrix: TickTick-faithful placement — axis-split urgency, category windows, Manual override (KC-21)
Refines the Eisenhower Matrix toward TickTick's "view over attributes" model: - Split the single quadrant override into two axes: importance is always Priority (High = top row); urgency is date-derived unless manually pinned via the new `urgencyOverride` flag. Horizontal drags pin urgency instead of fabricating/destroying a real due date; vertical drags set Priority directly. - A drag only flags "Manual" when the target column disagrees with the date-derived urgency, so dropping into a task's natural column stays on Auto. - Tiles show a "Manual" badge; the task menu gains "Reset Matrix Urgency". - Urgency window is now 3 days by default, widened to 14 for prep-heavy event types (birthday/domain/annual + subscription/renewal/exam/deadline titles). - New tasks no longer default to today — no date stays no date (not urgent). - Stored `quadrant` is kept synced to the computed placement so task color and widget accents follow the Matrix; Settings stats count by displayQuadrant. - Completed tasks are hidden from the Matrix grid by default (toggle to show). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,9 +23,9 @@ struct TaskItem: Identifiable, Codable, Equatable {
|
|||||||
// Recurrence: optional so existing saved tasks decode safely (missing key → nil).
|
// Recurrence: optional so existing saved tasks decode safely (missing key → nil).
|
||||||
var recurrenceEnd: Date? = nil // "repeat until" (nil = open-ended)
|
var recurrenceEnd: Date? = nil // "repeat until" (nil = open-ended)
|
||||||
var completedOccurrences: [String]? = nil // "yyyy-MM-dd" of completed occurrences
|
var completedOccurrences: [String]? = nil // "yyyy-MM-dd" of completed occurrences
|
||||||
// Matrix: a manual drag pins the task to a quadrant (overrides auto-placement).
|
// Matrix: horizontal manual moves override urgency only.
|
||||||
// nil = auto-place from Priority × deadline. Optional so old saves decode safely.
|
// nil = auto from deadline, true = force urgent, false = force not-urgent.
|
||||||
var matrixOverride: Quadrant? = nil
|
var urgencyOverride: Bool? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Priority: String, Codable, CaseIterable, Identifiable {
|
enum Priority: String, Codable, CaseIterable, Identifiable {
|
||||||
@@ -351,19 +351,20 @@ class TaskViewModel: ObservableObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Eisenhower Matrix (TickTick-style: Priority × deadline, with manual override)
|
// MARK: - Eisenhower Matrix (TickTick-style: Priority × deadline, with urgency override)
|
||||||
//
|
//
|
||||||
// The Matrix is a VIEW of tasks, not a separate system. A task is auto-placed:
|
// 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.
|
// • Importance (top row) = Priority is High; Medium/Low/None = bottom row.
|
||||||
// • Urgency (left column) = its (next active) due date is today/overdue/within
|
// • Urgency (left column) = its (next active) due date is today/overdue/within
|
||||||
// `urgentWindowDays`; undated or far-out = right column.
|
// `urgentWindowDays`; undated or far-out = right column.
|
||||||
// So Q1 = High + urgent, Q2 = High + not urgent, Q3 = not-High + urgent, Q4 = the rest.
|
// 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
|
// Vertical drag changes Priority directly. Horizontal drag sets `urgencyOverride`
|
||||||
// Priority to the target row — the move sticks and is never forced back. Editing the
|
// because we should not fabricate/destroy a real deadline just to move columns.
|
||||||
// Priority or due date in the task view clears the override so it re-places live.
|
// Editing the due date clears urgency override so the task re-places live.
|
||||||
// Recurring tasks roll forward (a completed occurrence is replaced by the next).
|
// Recurring tasks roll forward (a completed occurrence is replaced by the next).
|
||||||
|
|
||||||
static let urgentWindowDays = 7
|
static let urgentWindowDays = 3
|
||||||
|
static let categoryUrgentWindowDays = 14
|
||||||
|
|
||||||
private func isImportant(_ t: TaskItem) -> Bool { t.priority == .high }
|
private func isImportant(_ t: TaskItem) -> Bool { t.priority == .high }
|
||||||
|
|
||||||
@@ -374,11 +375,32 @@ class TaskViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func isUrgent(_ t: TaskItem, on today: Date) -> Bool {
|
private func isUrgent(_ t: TaskItem, on today: Date) -> Bool {
|
||||||
|
t.urgencyOverride ?? urgentByDeadline(t, on: today)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Date-derived urgency, ignoring any manual override (overdue/today/within window).
|
||||||
|
private func urgentByDeadline(_ t: TaskItem, on today: Date = Date()) -> Bool {
|
||||||
guard let due = effectiveDue(t, on: today) else { return false } // undated → never urgent
|
guard let due = effectiveDue(t, on: today) else { return false } // undated → never urgent
|
||||||
let days = Calendar.current.dateComponents([.day],
|
let days = Calendar.current.dateComponents([.day],
|
||||||
from: Calendar.current.startOfDay(for: today),
|
from: Calendar.current.startOfDay(for: today),
|
||||||
to: Calendar.current.startOfDay(for: due)).day ?? 0
|
to: Calendar.current.startOfDay(for: due)).day ?? 0
|
||||||
return days <= Self.urgentWindowDays // due soon / overdue
|
return days <= urgentWindow(for: t) // due soon / overdue
|
||||||
|
}
|
||||||
|
|
||||||
|
private func urgentWindow(for t: TaskItem) -> Int {
|
||||||
|
switch t.category {
|
||||||
|
case .birthday, .domain, .annual:
|
||||||
|
return Self.categoryUrgentWindowDays
|
||||||
|
case .reminder, .custom:
|
||||||
|
let title = t.title.lowercased()
|
||||||
|
if title.contains("domain") || title.contains("subscription")
|
||||||
|
|| title.contains("annual") || title.contains("renewal")
|
||||||
|
|| title.contains("renew") || title.contains("exam")
|
||||||
|
|| title.contains("deadline") {
|
||||||
|
return Self.categoryUrgentWindowDays
|
||||||
|
}
|
||||||
|
return Self.urgentWindowDays
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Auto-placement from Priority (importance) × deadline (urgency).
|
/// Auto-placement from Priority (importance) × deadline (urgency).
|
||||||
@@ -391,9 +413,9 @@ class TaskViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a task appears in the Matrix now: a manual override wins, else auto-placement.
|
/// Where a task appears in the Matrix now.
|
||||||
func displayQuadrant(_ t: TaskItem, on today: Date = Date()) -> Quadrant {
|
func displayQuadrant(_ t: TaskItem, on today: Date = Date()) -> Quadrant {
|
||||||
t.matrixOverride ?? autoQuadrant(t, on: today)
|
autoQuadrant(t, on: today)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Matrix rows for a quadrant, grouped by the *displayed* (computed) quadrant.
|
/// Matrix rows for a quadrant, grouped by the *displayed* (computed) quadrant.
|
||||||
@@ -436,36 +458,47 @@ class TaskViewModel: ObservableObject {
|
|||||||
func setDate(_ task: TaskItem, date: Date?) {
|
func setDate(_ task: TaskItem, date: Date?) {
|
||||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
tasks[idx].dueDate = date
|
tasks[idx].dueDate = date
|
||||||
tasks[idx].matrixOverride = nil // deadline changed → re-place from urgency
|
tasks[idx].urgencyOverride = nil // deadline changed → re-place from urgency
|
||||||
|
tasks[idx].quadrant = displayQuadrant(tasks[idx])
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCategory(_ task: TaskItem, category: TaskCategory) {
|
func setCategory(_ task: TaskItem, category: TaskCategory) {
|
||||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
tasks[idx].category = category
|
tasks[idx].category = category
|
||||||
|
tasks[idx].quadrant = displayQuadrant(tasks[idx])
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPriority(_ task: TaskItem, priority: Priority) {
|
func setPriority(_ task: TaskItem, priority: Priority) {
|
||||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
tasks[idx].priority = priority
|
tasks[idx].priority = priority
|
||||||
tasks[idx].matrixOverride = nil // priority changed → re-place from importance
|
tasks[idx].quadrant = displayQuadrant(tasks[idx]) // color follows placement
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manual drag/move in the Matrix. Pins the task to the target quadrant and syncs
|
/// Manual drag/move in the Matrix. Vertical movement updates Priority directly.
|
||||||
/// its Priority to that row — top row (Q1/Q2) becomes High; moving down out of the
|
/// Horizontal movement sets an urgency override instead of mutating due dates —
|
||||||
/// top demotes High→Medium so it reads as unimportant. TickTick-style: it sticks.
|
/// but only when the target column disagrees with the date-derived urgency, so a
|
||||||
|
/// drag into a task's natural column leaves it on Auto (no false "Manual" flag).
|
||||||
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
|
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
|
||||||
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
|
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
|
||||||
tasks[idx].matrixOverride = quadrant
|
|
||||||
tasks[idx].quadrant = quadrant // keep stored quadrant in sync (widget accent)
|
|
||||||
let importantRow = (quadrant == .urgent || quadrant == .schedule)
|
let importantRow = (quadrant == .urgent || quadrant == .schedule)
|
||||||
|
let urgentColumn = (quadrant == .urgent || quadrant == .delegate_)
|
||||||
if importantRow {
|
if importantRow {
|
||||||
tasks[idx].priority = .high
|
tasks[idx].priority = .high
|
||||||
} else if tasks[idx].priority == .high {
|
} else if tasks[idx].priority == .high {
|
||||||
tasks[idx].priority = .medium
|
tasks[idx].priority = .medium
|
||||||
}
|
}
|
||||||
|
tasks[idx].urgencyOverride = urgentByDeadline(tasks[idx]) == urgentColumn ? nil : urgentColumn
|
||||||
|
tasks[idx].quadrant = quadrant // keep stored quadrant in sync (color + widget accent)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetMatrixUrgencyOverride(_ task: TaskItem) {
|
||||||
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
|
tasks[idx].urgencyOverride = nil
|
||||||
|
tasks[idx].quadrant = displayQuadrant(tasks[idx])
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,6 +533,7 @@ class TaskViewModel: ObservableObject {
|
|||||||
priority: resolvedPriority, reminderDate: reminderDate,
|
priority: resolvedPriority, reminderDate: reminderDate,
|
||||||
constantReminder: constantReminder)
|
constantReminder: constantReminder)
|
||||||
item.recurrenceEnd = recurrenceEnd
|
item.recurrenceEnd = recurrenceEnd
|
||||||
|
item.quadrant = displayQuadrant(item) // color/widget identity follows placement
|
||||||
tasks.append(item)
|
tasks.append(item)
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
@@ -519,7 +553,8 @@ class TaskViewModel: ObservableObject {
|
|||||||
tasks[idx].reminderDate = reminderDate
|
tasks[idx].reminderDate = reminderDate
|
||||||
tasks[idx].constantReminder = constantReminder
|
tasks[idx].constantReminder = constantReminder
|
||||||
tasks[idx].recurrenceEnd = recurrenceEnd
|
tasks[idx].recurrenceEnd = recurrenceEnd
|
||||||
tasks[idx].matrixOverride = nil // editor saved a new deadline → re-place
|
tasks[idx].urgencyOverride = nil // editor saved a new deadline → re-place
|
||||||
|
tasks[idx].quadrant = displayQuadrant(tasks[idx])
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -607,6 +607,7 @@ struct CalendarView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onEditEvent: { editingEvent = EditableEvent(event: $0) },
|
onEditEvent: { editingEvent = EditableEvent(event: $0) },
|
||||||
onDeleteEvent: { calStore.deleteEvent($0) }
|
onDeleteEvent: { calStore.deleteEvent($0) }
|
||||||
)
|
)
|
||||||
@@ -1585,6 +1586,7 @@ struct DayTimelineView: View {
|
|||||||
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
||||||
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
||||||
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
||||||
|
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
|
||||||
// Calendar events (non-subscription only): edit/delete in the real calendar.
|
// Calendar events (non-subscription only): edit/delete in the real calendar.
|
||||||
var onEditEvent: ((EKEvent) -> Void)? = nil
|
var onEditEvent: ((EKEvent) -> Void)? = nil
|
||||||
var onDeleteEvent: ((EKEvent) -> Void)? = nil
|
var onDeleteEvent: ((EKEvent) -> Void)? = nil
|
||||||
@@ -1747,6 +1749,7 @@ struct DayTimelineView: View {
|
|||||||
onMove: { onMove?($0, $1) },
|
onMove: { onMove?($0, $1) },
|
||||||
onSetPriority: { onSetPriority?($0, $1) },
|
onSetPriority: { onSetPriority?($0, $1) },
|
||||||
onSetCategory: { onSetCategory?($0, $1) },
|
onSetCategory: { onSetCategory?($0, $1) },
|
||||||
|
onResetMatrixUrgency: onResetMatrixUrgency,
|
||||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||||
onEdit: onEdit,
|
onEdit: onEdit,
|
||||||
onDelete: { onDelete?($0) }
|
onDelete: { onDelete?($0) }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ struct MatrixView: View {
|
|||||||
@EnvironmentObject var taskVM: TaskViewModel
|
@EnvironmentObject var taskVM: TaskViewModel
|
||||||
@State private var showAddTask = false
|
@State private var showAddTask = false
|
||||||
@State private var dropTarget: Quadrant? = nil
|
@State private var dropTarget: Quadrant? = nil
|
||||||
@State private var hideCompleted = false
|
@State private var hideCompleted = true
|
||||||
@State private var showUserGuide = false
|
@State private var showUserGuide = false
|
||||||
@State private var drillQuadrant: Quadrant? = nil
|
@State private var drillQuadrant: Quadrant? = nil
|
||||||
@State private var showDrill = false
|
@State private var showDrill = false
|
||||||
@@ -67,6 +67,7 @@ struct MatrixView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
|
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -85,6 +86,7 @@ struct MatrixView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
|
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -107,6 +109,7 @@ struct MatrixView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
|
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -125,6 +128,7 @@ struct MatrixView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
|
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -188,6 +192,7 @@ struct QuadrantCard: View {
|
|||||||
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
||||||
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
||||||
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
||||||
|
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
|
||||||
var onDelete: ((TaskItem) -> Void)? = nil
|
var onDelete: ((TaskItem) -> Void)? = nil
|
||||||
var onTapHeader: (() -> Void)? = nil
|
var onTapHeader: (() -> Void)? = nil
|
||||||
|
|
||||||
@@ -265,6 +270,15 @@ struct QuadrantCard: View {
|
|||||||
.font(.system(size: 7, weight: .bold))
|
.font(.system(size: 7, weight: .bold))
|
||||||
.foregroundColor(badgeColor.opacity(0.7))
|
.foregroundColor(badgeColor.opacity(0.7))
|
||||||
}
|
}
|
||||||
|
if task.urgencyOverride != nil {
|
||||||
|
Text("Manual")
|
||||||
|
.font(AppFonts.mono(7, weight: .bold))
|
||||||
|
.foregroundColor(badgeColor)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(badgeBg)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
Text(task.title)
|
Text(task.title)
|
||||||
.font(AppFonts.sans(12, weight: .medium))
|
.font(AppFonts.sans(12, weight: .medium))
|
||||||
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
|
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
|
||||||
@@ -291,6 +305,7 @@ struct QuadrantCard: View {
|
|||||||
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
|
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
|
||||||
onSetPriority: { onSetPriority?($0, $1) },
|
onSetPriority: { onSetPriority?($0, $1) },
|
||||||
onSetCategory: { onSetCategory?($0, $1) },
|
onSetCategory: { onSetCategory?($0, $1) },
|
||||||
|
onResetMatrixUrgency: { t in withAnimation(KisaniSpring.snappy) { onResetMatrixUrgency?(t) } },
|
||||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||||
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
|
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
|
||||||
)
|
)
|
||||||
@@ -449,6 +464,7 @@ struct QuadrantDetailView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) }
|
onDelete: { taskVM.delete($0) }
|
||||||
@@ -474,6 +490,7 @@ struct QuadrantDetailView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) }
|
onDelete: { taskVM.delete($0) }
|
||||||
@@ -500,6 +517,7 @@ struct QuadrantDetailView: View {
|
|||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) }
|
onDelete: { taskVM.delete($0) }
|
||||||
@@ -622,12 +640,23 @@ private struct QDTaskRow: View {
|
|||||||
|
|
||||||
Button(action: onTap) {
|
Button(action: onTap) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text(task.title)
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
.font(AppFonts.sans(14, weight: task.isComplete ? .regular : .medium))
|
if task.urgencyOverride != nil {
|
||||||
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
|
Text("Manual")
|
||||||
.strikethrough(task.isComplete, color: AppColors.text3(cs))
|
.font(AppFonts.mono(8, weight: .bold))
|
||||||
.lineLimit(2)
|
.foregroundColor(quadrant.color)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.padding(.horizontal, 5)
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
.background(quadrant.softColor)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
Text(task.title)
|
||||||
|
.font(AppFonts.sans(14, weight: task.isComplete ? .regular : .medium))
|
||||||
|
.foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs))
|
||||||
|
.strikethrough(task.isComplete, color: AppColors.text3(cs))
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 3) {
|
VStack(alignment: .trailing, spacing: 3) {
|
||||||
if let d = task.dueDate {
|
if let d = task.dueDate {
|
||||||
|
|||||||
@@ -1051,7 +1051,7 @@ private struct ProfileStatsSheet: View {
|
|||||||
return taskVM.tasks.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= start }.count
|
return taskVM.tasks.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= start }.count
|
||||||
}
|
}
|
||||||
private var urgentOpen: Int {
|
private var urgentOpen: Int {
|
||||||
taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count
|
taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .urgent }.count
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 7-day task activity ──
|
// ── 7-day task activity ──
|
||||||
@@ -1140,10 +1140,10 @@ private struct ProfileStatsSheet: View {
|
|||||||
|
|
||||||
// Quadrant breakdown
|
// Quadrant breakdown
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
QuadStat(label: "Urgent", count: taskVM.tasks.filter { $0.quadrant == .urgent && !$0.isComplete }.count, color: AppColors.accent)
|
QuadStat(label: "Urgent", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .urgent }.count, color: AppColors.accent)
|
||||||
QuadStat(label: "Schedule", count: taskVM.tasks.filter { $0.quadrant == .schedule && !$0.isComplete }.count, color: AppColors.yellow)
|
QuadStat(label: "Schedule", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .schedule }.count, color: AppColors.yellow)
|
||||||
QuadStat(label: "Delegate", count: taskVM.tasks.filter { $0.quadrant == .delegate_ && !$0.isComplete }.count, color: AppColors.blue)
|
QuadStat(label: "Delegate", count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .delegate_ }.count, color: AppColors.blue)
|
||||||
QuadStat(label: "Eliminate",count: taskVM.tasks.filter { $0.quadrant == .eliminate && !$0.isComplete }.count, color: AppColors.green)
|
QuadStat(label: "Eliminate",count: taskVM.tasks.filter { !$0.isComplete && taskVM.displayQuadrant($0) == .eliminate }.count, color: AppColors.green)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(14).background(AppColors.surface(cs))
|
.padding(14).background(AppColors.surface(cs))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ struct TaskMenuItems: View {
|
|||||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||||
|
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
|
||||||
var onLiveActivity:(TaskItem) -> Void = { LiveActivityManager.toggle(for: $0) }
|
var onLiveActivity:(TaskItem) -> Void = { LiveActivityManager.toggle(for: $0) }
|
||||||
var onEdit: ((TaskItem) -> Void)? = nil
|
var onEdit: ((TaskItem) -> Void)? = nil
|
||||||
var onDelete: (TaskItem) -> Void = { _ in }
|
var onDelete: (TaskItem) -> Void = { _ in }
|
||||||
@@ -73,6 +74,12 @@ struct TaskMenuItems: View {
|
|||||||
}
|
}
|
||||||
} label: { Label("Move", systemImage: "arrow.right.square") }
|
} label: { Label("Move", systemImage: "arrow.right.square") }
|
||||||
|
|
||||||
|
if task.urgencyOverride != nil, let onResetMatrixUrgency {
|
||||||
|
Button { onResetMatrixUrgency(task) } label: {
|
||||||
|
Label("Reset Matrix Urgency", systemImage: "arrow.counterclockwise")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(Priority.allCases) { p in
|
ForEach(Priority.allCases) { p in
|
||||||
Button { onSetPriority(task, p) } label: {
|
Button { onSetPriority(task, p) } label: {
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ struct TodayView: View {
|
|||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
|
|
||||||
@@ -130,7 +131,8 @@ struct TodayView: View {
|
|||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
.padding(.top, 10)
|
.padding(.top, 10)
|
||||||
@@ -149,7 +151,8 @@ struct TodayView: View {
|
|||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +169,8 @@ struct TodayView: View {
|
|||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +187,8 @@ struct TodayView: View {
|
|||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +543,7 @@ struct OverdueCard: View {
|
|||||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||||
|
var onResetMatrixUrgency: (TaskItem) -> Void = { _ in }
|
||||||
|
|
||||||
@State private var expanded = true
|
@State private var expanded = true
|
||||||
|
|
||||||
@@ -639,6 +645,7 @@ struct OverdueCard: View {
|
|||||||
onMove: onMove,
|
onMove: onMove,
|
||||||
onSetPriority: onSetPriority,
|
onSetPriority: onSetPriority,
|
||||||
onSetCategory: onSetCategory,
|
onSetCategory: onSetCategory,
|
||||||
|
onResetMatrixUrgency: onResetMatrixUrgency,
|
||||||
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
|
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
|
||||||
onEdit: onEdit,
|
onEdit: onEdit,
|
||||||
onDelete: onDelete
|
onDelete: onDelete
|
||||||
@@ -951,6 +958,7 @@ struct UpcomingSection: View {
|
|||||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||||
|
var onResetMatrixUrgency: (TaskItem) -> Void = { _ in }
|
||||||
|
|
||||||
@State private var collapsed = false
|
@State private var collapsed = false
|
||||||
@State private var showAll = false
|
@State private var showAll = false
|
||||||
@@ -1014,6 +1022,7 @@ struct UpcomingSection: View {
|
|||||||
onMove: onMove,
|
onMove: onMove,
|
||||||
onSetPriority: onSetPriority,
|
onSetPriority: onSetPriority,
|
||||||
onSetCategory: onSetCategory,
|
onSetCategory: onSetCategory,
|
||||||
|
onResetMatrixUrgency: onResetMatrixUrgency,
|
||||||
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
|
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
|
||||||
onEdit: onEdit,
|
onEdit: onEdit,
|
||||||
onDelete: { task in withAnimation(KisaniSpring.snappy) { onDelete(task) } }
|
onDelete: { task in withAnimation(KisaniSpring.snappy) { onDelete(task) } }
|
||||||
@@ -1737,7 +1746,7 @@ struct AddTaskSheet: View {
|
|||||||
let importantRow = (prefilledQuadrant == .urgent || prefilledQuadrant == .schedule)
|
let importantRow = (prefilledQuadrant == .urgent || prefilledQuadrant == .schedule)
|
||||||
_selectedPriority = State(initialValue: importantRow ? .high : .none)
|
_selectedPriority = State(initialValue: importantRow ? .high : .none)
|
||||||
var p = NLParsed()
|
var p = NLParsed()
|
||||||
p.date = prefilledDate ?? Calendar.current.startOfDay(for: Date())
|
p.date = prefilledDate
|
||||||
_parsed = State(initialValue: p)
|
_parsed = State(initialValue: p)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,7 +1764,7 @@ struct AddTaskSheet: View {
|
|||||||
.frame(minHeight: 36, maxHeight: 72)
|
.frame(minHeight: 36, maxHeight: 72)
|
||||||
.onChange(of: rawText) { _ in
|
.onChange(of: rawText) { _ in
|
||||||
var p = NLTaskParser.parse(rawText)
|
var p = NLTaskParser.parse(rawText)
|
||||||
if p.date == nil { p.date = Calendar.current.startOfDay(for: Date()) }
|
if p.date == nil { p.date = prefilledDate }
|
||||||
withAnimation(KisaniSpring.micro) {
|
withAnimation(KisaniSpring.micro) {
|
||||||
parsed = p
|
parsed = p
|
||||||
if p.isBirthday { selectedCategory = .birthday }
|
if p.isBirthday { selectedCategory = .birthday }
|
||||||
|
|||||||
Reference in New Issue
Block a user