From 324ddf9b0a018a4565f992acff299d9b7d8143a9 Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Wed, 3 Jun 2026 13:51:33 +0300 Subject: [PATCH] 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 --- KisaniCal/Views/CalendarView.swift | 36 +++++++++++- KisaniCal/Views/TodayView.swift | 94 ++++++++++++++++++++++-------- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index 6cfade6..717d93a 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -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 { diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index 500aa21..48735a9 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -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()) } - .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") + } } }