Recurring tasks: occurrence engine on calendar + lists (KC-12)

Recurring tasks only ever showed on their single dueDate. Add a
recurrence engine that expands occurrences per-date with independent
per-occurrence completion.

- TaskItem: recurrenceEnd + completedOccurrences (optional, safe decode).
- TaskViewModel: isOccurrence/occurrenceComplete/toggleOccurrence/
  occurrenceCopy/nextOccurrence/occurrences(on:); Daily/Weekly/Monthly/
  Yearly/Every-Weekday; respects recurrenceEnd; works across years.
- toggle() routes recurring rows to per-occurrence completion.
- Date filters re-inject per-date occurrence copies; recurring excluded
  from overdue. Calendar dots + day detail use occurrences(on:).

Also log KC-11 (year display) + KC-12.

Pending: "repeat until" date UI (engine already honors recurrenceEnd).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-09 01:41:15 +03:00
parent 7b2b61a228
commit 6817d3e24a
4 changed files with 196 additions and 26 deletions

View File

@@ -463,9 +463,9 @@ struct CalendarView: View {
dots.append(calColor)
}
}
let taskDots = taskVM.tasks
// Includes recurring occurrences that land on this date.
let taskDots = taskVM.occurrences(on: date)
.filter { !$0.isComplete }
.filter { guard let d = $0.dueDate else { return false }; return cal.isDate(d, inSameDayAs: date) }
.prefix(max(0, 3 - dots.count))
.map { $0.quadrant.color }
dots.append(contentsOf: taskDots)
@@ -581,10 +581,7 @@ struct CalendarView: View {
ScrollView(showsIndicators: false) {
DayTimelineView(
date: selectedDate,
tasks: taskVM.tasks.filter { t in
guard let d = t.dueDate else { return false }
return cal.isDate(d, inSameDayAs: selectedDate)
},
tasks: taskVM.occurrences(on: selectedDate),
calEvents: calStore.isAuthorized ? calStore.events(for: selectedDate) : [],
workout: workoutVM.program(for: selectedDate),
workoutDone: workoutVM.isWorkoutComplete(on: selectedDate),