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?
|
||||
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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user