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:
Robin Kutesa
2026-06-03 13:51:33 +03:00
parent 646cdf6a9b
commit 324ddf9b0a
2 changed files with 104 additions and 26 deletions

View File

@@ -1454,6 +1454,10 @@ struct DayTimelineView: View {
let workout: WorkoutProgram?
var onWorkoutTap: (() -> 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 static let overdueFmt: DateFormatter = {
@@ -1573,7 +1577,7 @@ struct DayTimelineView: View {
} else {
VStack(spacing: 0) {
ForEach(Array(entries.enumerated()), id: \.element.id) { i, entry in
DayTLRow(
let row = DayTLRow(
entry: entry,
isFirst: i == 0,
isLast: i == entries.count - 1,
@@ -1581,10 +1585,40 @@ struct DayTimelineView: View {
onWorkoutTap: entry.workoutRef != nil ? onWorkoutTap : nil,
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 {

View File

@@ -9,6 +9,7 @@ struct TodayView: View {
@StateObject private var calStore = CalendarStore()
@State private var showAddTask = false
@State private var showWorkoutSession = false
@State private var editingTask: TaskItem? = nil
@AppStorage("todayHideCompleted") private var hideCompleted = false
@ObservedObject private var tm = TutorialManager.shared
@ObservedObject private var hk = HealthKitManager.shared
@@ -82,7 +83,11 @@ struct TodayView: View {
OverdueCard(
tasks: taskVM.overdueTasks,
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(.bottom, 8)
@@ -103,7 +108,11 @@ struct TodayView: View {
}
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)
@@ -188,6 +197,12 @@ struct TodayView: View {
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
.sheet(item: $editingTask) { task in
TaskEditSheet(task: task)
.environmentObject(taskVM)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
.onAppear {
calStore.refreshStatus()
calStore.fetchMonth(containing: Date())
@@ -459,6 +474,10 @@ struct OverdueCard: View {
let tasks: [TaskItem]
let onPostpone: () -> 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
@@ -500,46 +519,71 @@ struct OverdueCard: View {
ForEach(tasks) { task in
HStack(spacing: 10) {
ZStack {
Button { withAnimation(KisaniSpring.bounce) { onToggle(task) } } label: {
Circle()
.stroke(AppColors.accent, lineWidth: 1.5)
.stroke(AppColors.red, lineWidth: 1.5)
.frame(width: 20, height: 20)
}
.frame(width: 36, height: 44)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Text(task.title)
.font(AppFonts.sans(12.5))
.foregroundColor(AppColors.text(cs))
Spacer()
.font(AppFonts.sans(12.5, weight: .medium))
.foregroundColor(AppColors.red)
.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 {
Text(d, style: .date)
.font(AppFonts.mono(10))
.foregroundColor(AppColors.accent)
.foregroundColor(AppColors.red.opacity(0.7))
}
}
.padding(.horizontal, 10)
.contentShape(Rectangle())
.onTapGesture { onToggle(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")
.contextMenu { taskMenu(task) }
}
}
}
}
}
.background(AppColors.accent.opacity(0.07))
.background(AppColors.red.opacity(0.06))
.clipShape(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")
}
}
}