tasks: fix timed recurring tasks vanishing from Today after their time passes (KC-54)
Some checks failed
CI / build-and-test (push) Has been cancelled

Real bug: todayTasks excludes timed tasks whose time has passed ('belongs in
overdue'), but overdueTasks deliberately excludes ALL recurring tasks (it feeds
postpone/postponeAllOverdue, which mutate dueDate by id -- for a recurring task
that's the recurrence anchor, not just today's occurrence; including them there
would let Postpone All corrupt the whole series). Recurring tasks fell into the
gap between the two rules and vanished from the Today tab entirely once their
time passed, while Calendar's day view (no time-of-day gating) kept showing
them fine.

Fix: todayTasks only excludes past-time NON-recurring tasks. Recurring
occurrences always stay in Today, matching Calendar, without ever touching the
overdue/postpone path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-07-08 23:05:18 +03:00
parent 7bfa16aec4
commit 083a51065b
2 changed files with 44 additions and 2 deletions

View File

@@ -1790,3 +1790,40 @@ Files: `KisaniCalWidgets/KisaniCalWidgets.swift`.
Not build-verified (no iOS runtime here). To use: on the Lock Screen, tap the
widget area → Add Widgets → Wenza → Progress Dots → long-press → Edit Widget →
set Mode to This Day / This Week / This Year (This Month already configured).
---
## KC-54 — Timed recurring tasks vanish from Today once their time passes
**Status:** Fixed (not build-verified)
**Reported by:** User (screenshots: Calendar day view showed "Look into DPO
certificate" 11 AM and "Read Azure 104" 5 PM; neither appeared anywhere in the
Today tab — not in Today, not in Overdue, not in Completed)
### Root cause
Two rules interacted badly for **recurring** tasks:
- `todayTasks` deliberately excludes timed tasks whose time has passed today
("belongs in overdue, not today").
- `overdueTasks` deliberately excludes ALL recurring tasks (`!recurs($0)`),
because it feeds `postpone`/`postponeAllOverdue`, which mutate a task's
`dueDate` directly by id — for a recurring task that field is the
**recurrence rule's anchor date**, not just "today's occurrence." Including
recurring occurrences there would let "Postpone All" shift/corrupt the whole
series.
Net effect: once a recurring task's today-occurrence time passed, it was
excluded from Today (rule 1) but never picked up by Overdue (rule 2) — it fell
into a gap and disappeared from the Today tab entirely, while Calendar's day
view (`occurrences(on:)`, which has no time-of-day gating) kept showing it
normally. Not a display bug — real data was correctly stored, just not
surfaced.
### Fix
`todayTasks`' "past time → excluded" rule now only applies to non-recurring
tasks (`!$0.isRecurring`). A recurring task's today-occurrence always stays in
Today regardless of time — matching what Calendar already shows, and avoiding
the postpone/recurrence-anchor risk entirely (recurring rows are never routed
through `overdueTasks`). One-line, additive filter change; `activeRows()` /
`occurrenceCopy` already preserve `isRecurring` on the occurrence copy.
Files: `TaskItem.swift`.

View File

@@ -253,10 +253,15 @@ class TaskViewModel: ObservableObject {
let cal = Calendar.current
let start = cal.startOfDay(for: now)
let end = cal.date(byAdding: .day, value: 1, to: start)!
// Timed tasks whose time has already passed belong in overdue, not today.
// Timed ONE-OFF tasks whose time has already passed belong in overdue,
// not today. Recurring occurrences always stay in Today regardless of
// time: overdueTasks intentionally excludes recurring tasks (postponing
// one would shift the recurrence rule's anchor date, not just today's
// occurrence), so without this a past-time recurring task would vanish
// from the app entirely instead of showing here like Calendar shows it.
return activeRows().filter {
guard let d = $0.dueDate else { return false }
if $0.hasTime && d < now { return false }
if $0.hasTime && d < now && !$0.isRecurring { return false }
return d >= start && d < end
}
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }