Recurrence: add "repeat until" end date UI (KC-12)
Thread recurrenceEnd through NLParsed, TaskDatePickerSheet, the add/edit sheets, and addTask/updateTask. The date picker now shows a "Repeat until" row (graphical picker, default "Forever", with Clear) whenever a recurrence is set; the engine already honored recurrenceEnd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -366,7 +366,7 @@ Files: `CalendarView.swift`, `TodayView.swift`. Committed in `7b2b61a`.
|
||||
|
||||
## KC-12 — Recurring tasks don't appear on future dates
|
||||
|
||||
**Status:** In Progress (core engine done & build-verified; "repeat until" UI pending)
|
||||
**Status:** In Progress (implemented & build-verified, pending device build)
|
||||
**Reported by:** User
|
||||
**Area:** Tasks / Calendar
|
||||
|
||||
@@ -392,6 +392,10 @@ a single `dueDate`, and every surface matched by `isDate(dueDate, inSameDayAs:)`
|
||||
|
||||
Files: `TaskItem.swift`, `CalendarView.swift`.
|
||||
|
||||
### Pending
|
||||
- "Repeat until" date UI in the task date picker (thread `recurrenceEnd` through the
|
||||
add/edit sheets + `addTask`/`updateTask`). Engine already honors it.
|
||||
### "Repeat until" UI (done)
|
||||
- Added `recurrenceEnd` through `NLParsed`, `TaskDatePickerSheet`, both add/edit
|
||||
sheets, and `addTask`/`updateTask`.
|
||||
- Date picker shows a "Repeat until" row (graphical date picker, default "Forever",
|
||||
with Clear) whenever a recurrence is selected.
|
||||
|
||||
Files: `TaskItem.swift`, `CalendarView.swift`, `TodayView.swift`, `MatrixView.swift`.
|
||||
|
||||
@@ -368,21 +368,23 @@ class TaskViewModel: ObservableObject {
|
||||
recurrenceLabel: String? = nil,
|
||||
endDate: Date? = nil, isAllDay: Bool = false,
|
||||
priority: Priority = .none, reminderDate: Date? = nil,
|
||||
constantReminder: Bool = false) {
|
||||
tasks.append(TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
|
||||
quadrant: quadrant, category: category,
|
||||
taskColor: taskColor, isRecurring: isRecurring,
|
||||
recurrenceLabel: recurrenceLabel,
|
||||
endDate: endDate, isAllDay: isAllDay,
|
||||
priority: priority, reminderDate: reminderDate,
|
||||
constantReminder: constantReminder))
|
||||
constantReminder: Bool = false, recurrenceEnd: Date? = nil) {
|
||||
var item = TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
|
||||
quadrant: quadrant, category: category,
|
||||
taskColor: taskColor, isRecurring: isRecurring,
|
||||
recurrenceLabel: recurrenceLabel,
|
||||
endDate: endDate, isAllDay: isAllDay,
|
||||
priority: priority, reminderDate: reminderDate,
|
||||
constantReminder: constantReminder)
|
||||
item.recurrenceEnd = recurrenceEnd
|
||||
tasks.append(item)
|
||||
save()
|
||||
}
|
||||
|
||||
func updateTask(_ task: TaskItem, title: String, dueDate: Date?, hasTime: Bool,
|
||||
isRecurring: Bool, recurrenceLabel: String?,
|
||||
endDate: Date?, isAllDay: Bool, reminderDate: Date?,
|
||||
constantReminder: Bool = false) {
|
||||
constantReminder: Bool = false, recurrenceEnd: Date? = nil) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
tasks[idx].title = title
|
||||
tasks[idx].dueDate = dueDate
|
||||
@@ -393,6 +395,7 @@ class TaskViewModel: ObservableObject {
|
||||
tasks[idx].isAllDay = isAllDay
|
||||
tasks[idx].reminderDate = reminderDate
|
||||
tasks[idx].constantReminder = constantReminder
|
||||
tasks[idx].recurrenceEnd = recurrenceEnd
|
||||
save()
|
||||
}
|
||||
|
||||
|
||||
@@ -675,6 +675,7 @@ struct TaskEditSheet: View {
|
||||
@State private var hasTime: Bool
|
||||
@State private var isRecurring: Bool
|
||||
@State private var recurrenceLabel: String?
|
||||
@State private var recurrenceEnd: Date?
|
||||
@State private var endDate: Date?
|
||||
@State private var isAllDay: Bool
|
||||
@State private var reminderDate: Date?
|
||||
@@ -688,6 +689,7 @@ struct TaskEditSheet: View {
|
||||
_hasTime = State(initialValue: task.hasTime)
|
||||
_isRecurring = State(initialValue: task.isRecurring)
|
||||
_recurrenceLabel = State(initialValue: task.recurrenceLabel)
|
||||
_recurrenceEnd = State(initialValue: task.recurrenceEnd)
|
||||
_endDate = State(initialValue: task.endDate)
|
||||
_isAllDay = State(initialValue: task.isAllDay)
|
||||
_reminderDate = State(initialValue: task.reminderDate)
|
||||
@@ -838,7 +840,8 @@ struct TaskEditSheet: View {
|
||||
endDate: $endDate,
|
||||
isAllDay: $isAllDay,
|
||||
reminderDate: $reminderDate,
|
||||
constantReminder: $constantReminder
|
||||
constantReminder: $constantReminder,
|
||||
recurrenceEnd: $recurrenceEnd
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -849,7 +852,7 @@ struct TaskEditSheet: View {
|
||||
taskVM.updateTask(task, title: t, dueDate: dueDate, hasTime: hasTime,
|
||||
isRecurring: isRecurring, recurrenceLabel: recurrenceLabel,
|
||||
endDate: endDate, isAllDay: isAllDay, reminderDate: reminderDate,
|
||||
constantReminder: constantReminder)
|
||||
constantReminder: constantReminder, recurrenceEnd: recurrenceEnd)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,6 +1304,7 @@ struct NLParsed {
|
||||
var tokenRanges: [NSRange] = []
|
||||
var isRecurring: Bool = false
|
||||
var recurrenceLabel: String? = nil
|
||||
var recurrenceEnd: Date? = nil
|
||||
var endDate: Date? = nil
|
||||
var isAllDay: Bool = false
|
||||
var isBirthday: Bool = false
|
||||
@@ -1807,7 +1808,8 @@ struct AddTaskSheet: View {
|
||||
endDate: $parsed.endDate,
|
||||
isAllDay: $parsed.isAllDay,
|
||||
reminderDate: $reminderDate,
|
||||
constantReminder: $constantReminder
|
||||
constantReminder: $constantReminder,
|
||||
recurrenceEnd: $parsed.recurrenceEnd
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1818,7 +1820,8 @@ struct AddTaskSheet: View {
|
||||
quadrant: selectedQuadrant, category: selectedCategory,
|
||||
isRecurring: parsed.isRecurring, recurrenceLabel: parsed.recurrenceLabel,
|
||||
endDate: parsed.endDate, isAllDay: parsed.isAllDay,
|
||||
reminderDate: reminderDate, constantReminder: constantReminder)
|
||||
reminderDate: reminderDate, constantReminder: constantReminder,
|
||||
recurrenceEnd: parsed.recurrenceEnd)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -1852,6 +1855,7 @@ struct TaskDatePickerSheet: View {
|
||||
@Binding var hasTime: Bool
|
||||
@Binding var isRecurring: Bool
|
||||
@Binding var recurrenceLabel: String?
|
||||
@Binding var recurrenceEnd: Date?
|
||||
@Binding var endDate: Date?
|
||||
@Binding var isAllDay: Bool
|
||||
@Binding var reminderDate: Date?
|
||||
@@ -1876,6 +1880,8 @@ struct TaskDatePickerSheet: View {
|
||||
@State private var customHour: Int
|
||||
@State private var customMinute: Int
|
||||
@State private var localRepeat: String
|
||||
@State private var localRecurrenceEnd: Date?
|
||||
@State private var showRecurrenceEndPicker = false
|
||||
@State private var localIsAllDay: Bool
|
||||
@State private var editingEnd = false
|
||||
|
||||
@@ -1883,11 +1889,13 @@ struct TaskDatePickerSheet: View {
|
||||
isRecurring: Binding<Bool>, recurrenceLabel: Binding<String?>,
|
||||
endDate: Binding<Date?>, isAllDay: Binding<Bool>,
|
||||
reminderDate: Binding<Date?> = .constant(nil),
|
||||
constantReminder: Binding<Bool> = .constant(false)) {
|
||||
constantReminder: Binding<Bool> = .constant(false),
|
||||
recurrenceEnd: Binding<Date?> = .constant(nil)) {
|
||||
_selectedDate = selectedDate
|
||||
_hasTime = hasTime
|
||||
_isRecurring = isRecurring
|
||||
_recurrenceLabel = recurrenceLabel
|
||||
_recurrenceEnd = recurrenceEnd
|
||||
_endDate = endDate
|
||||
_isAllDay = isAllDay
|
||||
_reminderDate = reminderDate
|
||||
@@ -1898,6 +1906,7 @@ struct TaskDatePickerSheet: View {
|
||||
_localEndDate = State(initialValue: endDate.wrappedValue ?? start)
|
||||
_localTime = State(initialValue: hasTime.wrappedValue ? selectedDate.wrappedValue : nil)
|
||||
_localRepeat = State(initialValue: recurrenceLabel.wrappedValue ?? "None")
|
||||
_localRecurrenceEnd = State(initialValue: recurrenceEnd.wrappedValue)
|
||||
_localIsAllDay = State(initialValue: isAllDay.wrappedValue)
|
||||
_pickerMode = State(initialValue: endDate.wrappedValue != nil ? 1 : 0)
|
||||
_localConstant = State(initialValue: constantReminder.wrappedValue)
|
||||
@@ -2048,6 +2057,32 @@ struct TaskDatePickerSheet: View {
|
||||
value: repeatDisplayValue, active: localRepeat != "None")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// ── Repeat until (only when a recurrence is set) ──
|
||||
if localRepeat != "None" {
|
||||
Divider().padding(.leading, 54)
|
||||
Button { withAnimation(KisaniSpring.micro) { showRecurrenceEndPicker.toggle() } } label: {
|
||||
pickerRow(icon: "calendar.badge.exclamationmark", label: "Repeat until",
|
||||
value: localRecurrenceEnd.map { repeatEndFmt.string(from: $0) } ?? "Forever",
|
||||
active: localRecurrenceEnd != nil)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
if showRecurrenceEndPicker {
|
||||
DatePicker("", selection: Binding(
|
||||
get: { localRecurrenceEnd ?? localDate },
|
||||
set: { localRecurrenceEnd = $0 }
|
||||
), in: localDate..., displayedComponents: .date)
|
||||
.datePickerStyle(.graphical).labelsHidden()
|
||||
.padding(.horizontal, 8)
|
||||
if localRecurrenceEnd != nil {
|
||||
Button { withAnimation(KisaniSpring.micro) { localRecurrenceEnd = nil; showRecurrenceEndPicker = false } } label: {
|
||||
Text("Clear end date").font(AppFonts.sans(13)).foregroundColor(AppColors.accent)
|
||||
}
|
||||
.frame(maxWidth: .infinity).padding(.bottom, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color(uiColor: cs == .dark ? .secondarySystemBackground : .systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||
@@ -2453,6 +2488,10 @@ struct TaskDatePickerSheet: View {
|
||||
return "Duration: \(n) day\(n == 1 ? "" : "s")"
|
||||
}
|
||||
|
||||
private let repeatEndFmt: DateFormatter = {
|
||||
let f = DateFormatter(); f.dateFormat = "MMM d, yyyy"; return f
|
||||
}()
|
||||
|
||||
private func commit() {
|
||||
if pickerMode == 0 {
|
||||
var final = Calendar.current.startOfDay(for: localDate)
|
||||
@@ -2469,6 +2508,7 @@ struct TaskDatePickerSheet: View {
|
||||
isAllDay = false
|
||||
isRecurring = localRepeat != "None"
|
||||
recurrenceLabel = localRepeat != "None" ? localRepeat : nil
|
||||
recurrenceEnd = localRepeat != "None" ? localRecurrenceEnd : nil
|
||||
// Reminder is a lead-time offset from the task date at the chosen time-of-day.
|
||||
if let off = localReminderOffset {
|
||||
reminderDate = computedReminderDate(offset: off)
|
||||
|
||||
Reference in New Issue
Block a user