feat: overdue tasks in red with quick postpone + task context menu
- Overdue card: red task titles, per-row "+1d" postpone, tappable complete. - Task rows (overdue + day timeline) gain a press menu: Complete, Postpone, Pin/Unpin, Edit, Share, Delete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1454,6 +1454,10 @@ struct DayTimelineView: View {
|
|||||||
let workout: WorkoutProgram?
|
let workout: WorkoutProgram?
|
||||||
var onWorkoutTap: (() -> Void)? = nil
|
var onWorkoutTap: (() -> Void)? = nil
|
||||||
var onToggle: ((TaskItem) -> Void)? = nil
|
var onToggle: ((TaskItem) -> Void)? = nil
|
||||||
|
var onPin: ((TaskItem) -> Void)? = nil
|
||||||
|
var onPostpone: ((TaskItem) -> Void)? = nil
|
||||||
|
var onEdit: ((TaskItem) -> Void)? = nil
|
||||||
|
var onDelete: ((TaskItem) -> Void)? = nil
|
||||||
|
|
||||||
private let cal = Calendar.current
|
private let cal = Calendar.current
|
||||||
private static let overdueFmt: DateFormatter = {
|
private static let overdueFmt: DateFormatter = {
|
||||||
@@ -1573,7 +1577,7 @@ struct DayTimelineView: View {
|
|||||||
} else {
|
} else {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ForEach(Array(entries.enumerated()), id: \.element.id) { i, entry in
|
ForEach(Array(entries.enumerated()), id: \.element.id) { i, entry in
|
||||||
DayTLRow(
|
let row = DayTLRow(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
isFirst: i == 0,
|
isFirst: i == 0,
|
||||||
isLast: i == entries.count - 1,
|
isLast: i == entries.count - 1,
|
||||||
@@ -1581,10 +1585,40 @@ struct DayTimelineView: View {
|
|||||||
onWorkoutTap: entry.workoutRef != nil ? onWorkoutTap : nil,
|
onWorkoutTap: entry.workoutRef != nil ? onWorkoutTap : nil,
|
||||||
onToggle: entry.taskRef.map { t in { onToggle?(t) } }
|
onToggle: entry.taskRef.map { t in { onToggle?(t) } }
|
||||||
)
|
)
|
||||||
|
if let t = entry.taskRef {
|
||||||
|
row.contextMenu { taskContextMenu(t) }
|
||||||
|
} else {
|
||||||
|
row
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func taskContextMenu(_ task: TaskItem) -> some View {
|
||||||
|
Button { onToggle?(task) } label: {
|
||||||
|
Label(task.isComplete ? "Mark Incomplete" : "Mark Complete", systemImage: "checkmark.circle")
|
||||||
|
}
|
||||||
|
if let onPostpone {
|
||||||
|
Button { onPostpone(task) } label: {
|
||||||
|
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let onPin {
|
||||||
|
Button { onPin(task) } label: {
|
||||||
|
Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let onEdit {
|
||||||
|
Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") }
|
||||||
|
}
|
||||||
|
ShareLink(item: task.title) { Label("Share", systemImage: "square.and.arrow.up") }
|
||||||
|
if let onDelete {
|
||||||
|
Divider()
|
||||||
|
Button(role: .destructive) { onDelete(task) } label: { Label("Delete", systemImage: "trash") }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct DayTLRow: View {
|
private struct DayTLRow: View {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ struct TodayView: View {
|
|||||||
@StateObject private var calStore = CalendarStore()
|
@StateObject private var calStore = CalendarStore()
|
||||||
@State private var showAddTask = false
|
@State private var showAddTask = false
|
||||||
@State private var showWorkoutSession = false
|
@State private var showWorkoutSession = false
|
||||||
|
@State private var editingTask: TaskItem? = nil
|
||||||
@AppStorage("todayHideCompleted") private var hideCompleted = false
|
@AppStorage("todayHideCompleted") private var hideCompleted = false
|
||||||
@ObservedObject private var tm = TutorialManager.shared
|
@ObservedObject private var tm = TutorialManager.shared
|
||||||
@ObservedObject private var hk = HealthKitManager.shared
|
@ObservedObject private var hk = HealthKitManager.shared
|
||||||
@@ -82,7 +83,11 @@ struct TodayView: View {
|
|||||||
OverdueCard(
|
OverdueCard(
|
||||||
tasks: taskVM.overdueTasks,
|
tasks: taskVM.overdueTasks,
|
||||||
onPostpone: { taskVM.postponeAllOverdue() },
|
onPostpone: { taskVM.postponeAllOverdue() },
|
||||||
onToggle: { task in withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } }
|
onToggle: { task in withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } },
|
||||||
|
onPostponeTask: { taskVM.postpone($0) },
|
||||||
|
onPin: { taskVM.pin($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
|
onDelete: { taskVM.delete($0) }
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
@@ -103,7 +108,11 @@ struct TodayView: View {
|
|||||||
}
|
}
|
||||||
showWorkoutSession = true
|
showWorkoutSession = true
|
||||||
},
|
},
|
||||||
onToggle: { task in withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } }
|
onToggle: { task in withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } },
|
||||||
|
onPin: { taskVM.pin($0) },
|
||||||
|
onPostpone: { taskVM.postpone($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
|
onDelete: { taskVM.delete($0) }
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
|
|
||||||
@@ -188,6 +197,12 @@ struct TodayView: View {
|
|||||||
.presentationDetents([.large])
|
.presentationDetents([.large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
}
|
}
|
||||||
|
.sheet(item: $editingTask) { task in
|
||||||
|
TaskEditSheet(task: task)
|
||||||
|
.environmentObject(taskVM)
|
||||||
|
.presentationDetents([.medium, .large])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
calStore.refreshStatus()
|
calStore.refreshStatus()
|
||||||
calStore.fetchMonth(containing: Date())
|
calStore.fetchMonth(containing: Date())
|
||||||
@@ -459,6 +474,10 @@ struct OverdueCard: View {
|
|||||||
let tasks: [TaskItem]
|
let tasks: [TaskItem]
|
||||||
let onPostpone: () -> Void
|
let onPostpone: () -> Void
|
||||||
let onToggle: (TaskItem) -> Void
|
let onToggle: (TaskItem) -> Void
|
||||||
|
var onPostponeTask: (TaskItem) -> Void = { _ in }
|
||||||
|
var onPin: (TaskItem) -> Void = { _ in }
|
||||||
|
var onEdit: (TaskItem) -> Void = { _ in }
|
||||||
|
var onDelete: (TaskItem) -> Void = { _ in }
|
||||||
|
|
||||||
@State private var expanded = true
|
@State private var expanded = true
|
||||||
|
|
||||||
@@ -500,46 +519,71 @@ struct OverdueCard: View {
|
|||||||
|
|
||||||
ForEach(tasks) { task in
|
ForEach(tasks) { task in
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
ZStack {
|
Button { withAnimation(KisaniSpring.bounce) { onToggle(task) } } label: {
|
||||||
Circle()
|
Circle()
|
||||||
.stroke(AppColors.accent, lineWidth: 1.5)
|
.stroke(AppColors.red, lineWidth: 1.5)
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
|
.frame(width: 36, height: 44)
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.frame(width: 36, height: 44)
|
.buttonStyle(.plain)
|
||||||
.contentShape(Rectangle())
|
|
||||||
|
|
||||||
Text(task.title)
|
Text(task.title)
|
||||||
.font(AppFonts.sans(12.5))
|
.font(AppFonts.sans(12.5, weight: .medium))
|
||||||
.foregroundColor(AppColors.text(cs))
|
.foregroundColor(AppColors.red)
|
||||||
Spacer()
|
.lineLimit(1)
|
||||||
|
Spacer(minLength: 6)
|
||||||
|
|
||||||
|
// Small inline postpone
|
||||||
|
Button { withAnimation(KisaniSpring.snappy) { onPostponeTask(task) } } label: {
|
||||||
|
Text("+1d")
|
||||||
|
.font(AppFonts.mono(9, weight: .bold))
|
||||||
|
.foregroundColor(AppColors.red)
|
||||||
|
.padding(.horizontal, 7).padding(.vertical, 3)
|
||||||
|
.background(AppColors.redSoft)
|
||||||
|
.cornerRadius(5)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
if let d = task.dueDate {
|
if let d = task.dueDate {
|
||||||
Text(d, style: .date)
|
Text(d, style: .date)
|
||||||
.font(AppFonts.mono(10))
|
.font(AppFonts.mono(10))
|
||||||
.foregroundColor(AppColors.accent)
|
.foregroundColor(AppColors.red.opacity(0.7))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 10)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { onToggle(task) }
|
.contextMenu { taskMenu(task) }
|
||||||
.contextMenu {
|
|
||||||
Button {
|
|
||||||
withAnimation { onToggle(task) }
|
|
||||||
} label: {
|
|
||||||
Label("Mark Complete", systemImage: "checkmark.circle")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
// postpone this single task via the viewmodel — accessed through onToggle pattern
|
|
||||||
} label: {
|
|
||||||
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(AppColors.accent.opacity(0.07))
|
.background(AppColors.red.opacity(0.06))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
|
||||||
.overlay(RoundedRectangle(cornerRadius: AppRadius.large)
|
.overlay(RoundedRectangle(cornerRadius: AppRadius.large)
|
||||||
.stroke(AppColors.accent.opacity(0.18), lineWidth: 1))
|
.stroke(AppColors.red.opacity(0.18), lineWidth: 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func taskMenu(_ task: TaskItem) -> some View {
|
||||||
|
Button { withAnimation { onToggle(task) } } label: {
|
||||||
|
Label("Mark Complete", systemImage: "checkmark.circle")
|
||||||
|
}
|
||||||
|
Button { onPostponeTask(task) } label: {
|
||||||
|
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
||||||
|
}
|
||||||
|
Button { onPin(task) } label: {
|
||||||
|
Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin")
|
||||||
|
}
|
||||||
|
Button { onEdit(task) } label: {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
ShareLink(item: task.title) {
|
||||||
|
Label("Share", systemImage: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button(role: .destructive) { onDelete(task) } label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user