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:
@@ -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