From 7881754fda05a21bb80bf475a05e05e2f152d2a6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 24 Mar 2026 09:41:13 +0100 Subject: [PATCH] =?UTF-8?q?docs(11):=20create=20phase=20plan=20for=20tasks?= =?UTF-8?q?=20management=20=E2=80=94=20anytime=20completion=20and=20pre-po?= =?UTF-8?q?pulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/REQUIREMENTS.md | 37 +- .planning/ROADMAP.md | 30 +- .../11-01-PLAN.md | 252 ++++++++ .../11-02-PLAN.md | 566 ++++++++++++++++++ 4 files changed, 868 insertions(+), 17 deletions(-) create mode 100644 .planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-01-PLAN.md create mode 100644 .planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-02-PLAN.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index e8149d8..417d333 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -25,6 +25,14 @@ Requirements for milestone v1.2 Polish & Task Management. Each maps to roadmap p - [x] **CLN-01**: Dead code from v1.0 daily plan (daily_plan_providers.dart, daily_plan_task_row.dart, progress_card.dart) is removed without breaking notification service (DailyPlanDao must be preserved) +### Tasks Management (Phase 11) + +- [ ] **TM-01**: User can check off (complete) a task on any calendar day — checkboxes are never disabled for future dates +- [ ] **TM-02**: When completing a task on a non-due day, nextDueDate is recalculated from today (not from the original due date) +- [ ] **TM-03**: Recurring tasks are pre-populated on all applicable days within their current interval window (e.g., a weekly task shows every day in the 7-day window leading up to its due date) +- [ ] **TM-04**: Pre-populated tasks that have already been completed in the current interval period are hidden from the calendar view +- [ ] **TM-05**: Pre-populated tasks not yet due have a muted visual distinction (reduced opacity) compared to due-today and overdue tasks + ## Future Requirements Deferred to future release. Tracked but not in current roadmap. @@ -71,21 +79,26 @@ Which phases cover which requirements. Updated during roadmap creation. | Requirement | Phase | Status | |-------------|-------|--------| -| DEL-01 | Phase 8 | Planned | -| DEL-02 | Phase 8 | Planned | -| DEL-03 | Phase 8 | Planned | -| DEL-04 | Phase 8 | Planned | -| TCX-01 | Phase 9 | Planned | -| TCX-02 | Phase 9 | Planned | -| TCX-03 | Phase 9 | Planned | -| TCX-04 | Phase 9 | Planned | -| CLN-01 | Phase 10 | Planned | +| DEL-01 | Phase 8 | Complete | +| DEL-02 | Phase 8 | Complete | +| DEL-03 | Phase 8 | Complete | +| DEL-04 | Phase 8 | Complete | +| TCX-01 | Phase 9 | Complete | +| TCX-02 | Phase 9 | Complete | +| TCX-03 | Phase 9 | Complete | +| TCX-04 | Phase 9 | Complete | +| CLN-01 | Phase 10 | Complete | +| TM-01 | Phase 11 | Planned | +| TM-02 | Phase 11 | Planned | +| TM-03 | Phase 11 | Planned | +| TM-04 | Phase 11 | Planned | +| TM-05 | Phase 11 | Planned | **Coverage:** -- v1.2 requirements: 9 total -- Mapped to phases: 9 +- v1.2 requirements: 14 total +- Mapped to phases: 14 - Unmapped: 0 --- *Requirements defined: 2026-03-18* -*Last updated: 2026-03-18 after roadmap creation (phases 8-10)* +*Last updated: 2026-03-24 after phase 11 planning* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 6fd291a..9160e54 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -4,7 +4,7 @@ - ✅ **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16) - ✅ **v1.1 Calendar & Polish** — Phases 5-7 (shipped 2026-03-16) -- **v1.2 Polish & Task Management** — Phases 8-10 (in progress) +- **v1.2 Polish & Task Management** — Phases 8-11 (in progress) ## Phases @@ -31,11 +31,12 @@ See `milestones/v1.1-ROADMAP.md` for full phase details. -**v1.2 Polish & Task Management (Phases 8-10):** +**v1.2 Polish & Task Management (Phases 8-11):** - [x] **Phase 8: Task Delete** - Add smart delete action to tasks — hard delete if never completed, soft delete (deactivate) if completed at least once (completed 2026-03-18) - [x] **Phase 9: Task Creation UX** - Rework the frequency picker from flat preset chips to an intuitive "Every N units" interface with quick-select shortcuts (completed 2026-03-18) - [x] **Phase 10: Dead Code Cleanup** - Remove orphaned v1.0 daily plan files and verify no regressions (completed 2026-03-19) +- [ ] **Phase 11: Tasks Management** - Allow task checking anytime and pre-populate recurring tasks within their interval window ## Phase Details @@ -81,6 +82,24 @@ Plans: 3. All 108+ tests pass after cleanup 4. `dart analyze` reports zero issues +### Phase 11: Tasks Management - Allow task checking anytime and pre-populate recurring tasks +**Goal**: Users can complete tasks on any day regardless of schedule, and recurring tasks appear on all applicable days within their interval window — making the app feel like a consistent checklist rather than a rigid scheduler +**Depends on**: Phase 10 +**Requirements**: TM-01, TM-02, TM-03, TM-04, TM-05 +**Plans:** 2 plans +Plans: +- [ ] 11-01-PLAN.md — Anytime completion: remove checkbox restrictions, recalculate nextDueDate from today on non-due-day completion +- [ ] 11-02-PLAN.md — Pre-population: virtual task instances in provider layer, period-completion filtering, muted visual styling +**Success Criteria** (what must be TRUE): + 1. Checkboxes are always enabled on all calendar days (past, today, future) and in room task lists + 2. Completing a task on a non-due day recalculates nextDueDate from today, not the original due date + 3. A weekly task appears on all 7 days leading up to its due date + 4. A monthly task appears on all days within its current month interval + 5. Tasks already completed in the current interval period do not reappear as pre-populated + 6. Pre-populated tasks have a visually muted appearance (reduced opacity) compared to due-today tasks + 7. Overdue tasks retain their existing coral accent styling + 8. All tests pass and dart analyze reports zero issues + ## Progress | Phase | Milestone | Plans Complete | Status | Completed | @@ -92,6 +111,7 @@ Plans: | 5. Calendar Strip | v1.1 | 2/2 | Complete | 2026-03-16 | | 6. Task History | v1.1 | 1/1 | Complete | 2026-03-16 | | 7. Task Sorting | v1.1 | 2/2 | Complete | 2026-03-16 | -| 8. Task Delete | 2/2 | Complete | 2026-03-18 | - | -| 9. Task Creation UX | 1/1 | Complete | 2026-03-18 | - | -| 10. Dead Code Cleanup | v1.2 | Complete | 2026-03-19 | 2026-03-19 | +| 8. Task Delete | v1.2 | 2/2 | Complete | 2026-03-18 | +| 9. Task Creation UX | v1.2 | 1/1 | Complete | 2026-03-18 | +| 10. Dead Code Cleanup | v1.2 | 1/1 | Complete | 2026-03-19 | +| 11. Tasks Management | v1.2 | 0/2 | Planned | - | diff --git a/.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-01-PLAN.md b/.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-01-PLAN.md new file mode 100644 index 0000000..e8f30f8 --- /dev/null +++ b/.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-01-PLAN.md @@ -0,0 +1,252 @@ +--- +phase: 11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - lib/features/home/presentation/calendar_day_list.dart + - lib/features/home/presentation/calendar_task_row.dart + - lib/features/tasks/presentation/task_row.dart + - lib/features/tasks/data/tasks_dao.dart + - test/features/tasks/data/tasks_dao_test.dart +autonomous: true +requirements: + - TM-01 + - TM-02 + +must_haves: + truths: + - "User can check off a task on any calendar day including future days" + - "When completing a task on a non-due day, nextDueDate recalculates from today not from the original due date" + - "Overdue tasks remain completable (no regression)" + artifacts: + - path: "lib/features/home/presentation/calendar_day_list.dart" + provides: "Checkbox always enabled for all day tasks" + contains: "canComplete: true" + - path: "lib/features/home/presentation/calendar_task_row.dart" + provides: "CalendarTaskRow with always-enabled checkbox" + contains: "canComplete" + - path: "lib/features/tasks/presentation/task_row.dart" + provides: "TaskRow with always-enabled checkbox" + - path: "lib/features/tasks/data/tasks_dao.dart" + provides: "completeTask with today-based recalculation" + contains: "calculateNextDueDate" + key_links: + - from: "lib/features/home/presentation/calendar_day_list.dart" + to: "CalendarTaskRow" + via: "canComplete: true always passed" + pattern: "canComplete: true" + - from: "lib/features/tasks/data/tasks_dao.dart" + to: "scheduling.dart" + via: "calculateNextDueDate uses today as base when completing on non-due day" + pattern: "calculateNextDueDate" +--- + + +Enable anytime task completion: remove all checkbox-disable restrictions so users can mark tasks done on any calendar day (past, today, or future). When completing a task on a non-due day, recalculate nextDueDate from today (per D-02). + +Purpose: Users should never be blocked from marking a task as done. The current behavior of disabling checkboxes for future tasks creates friction and confusion. +Output: All checkboxes always enabled. completeTask() uses today as base for nextDueDate calculation when task is completed on a non-due day. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-CONTEXT.md + + + + +From lib/features/home/presentation/calendar_task_row.dart: +```dart +class CalendarTaskRow extends StatelessWidget { + const CalendarTaskRow({ + super.key, + required this.taskWithRoom, + required this.onCompleted, + this.isOverdue = false, + this.showRoomTag = true, + this.canComplete = true, // Currently defaults to true but overridden with !isFuture + }); + final bool canComplete; // When false, checkbox is disabled +} +``` + +From lib/features/tasks/data/tasks_dao.dart: +```dart +Future completeTask(int taskId, {DateTime? now}) { + // Step 3: calculates next due from task.nextDueDate (original due date) + var nextDue = calculateNextDueDate( + currentDueDate: task.nextDueDate, // <-- This is the line to change for non-due-day + ... + ); +} +``` + +From lib/features/tasks/domain/scheduling.dart: +```dart +DateTime calculateNextDueDate({ + required DateTime currentDueDate, + required IntervalType intervalType, + required int intervalDays, + int? anchorDay, +}); + +DateTime catchUpToPresent({ + required DateTime nextDue, + required DateTime today, + required IntervalType intervalType, + required int intervalDays, + int? anchorDay, +}); +``` + + + + + + + Task 1: Remove checkbox-disable restrictions in all three UI files + + lib/features/home/presentation/calendar_day_list.dart + lib/features/home/presentation/calendar_task_row.dart + lib/features/tasks/presentation/task_row.dart + + + lib/features/home/presentation/calendar_day_list.dart + lib/features/home/presentation/calendar_task_row.dart + lib/features/tasks/presentation/task_row.dart + + +**Per D-01: Remove isFuture / canComplete restrictions.** + +1. **calendar_day_list.dart** (line ~271): In the `_buildTaskList` method, change the day tasks loop to always pass `canComplete: true`: + - Remove the line `final isFuture = state.selectedDate.isAfter(today);` (line ~245) + - Change `canComplete: !isFuture` (line ~271) to `canComplete: true` + - The `isFuture` variable can be removed entirely since it is only used for `canComplete` + - Keep the `today` variable — it is still used for `isToday` check in _buildContent + +2. **calendar_task_row.dart**: No changes needed. The `canComplete` parameter already defaults to `true` and the widget itself has no internal disable logic. The restriction was applied by the caller (calendar_day_list.dart). + +3. **task_row.dart** (lines ~45, ~60-62): Remove the `isFuture` check that disables the checkbox: + - Remove line `final isFuture = dueDate.isAfter(today);` (line ~45) + - Change the `onChanged` from the ternary `isFuture ? null : (_) { ... }` to always-enabled: + ```dart + onChanged: (_) { + ref.read(taskActionsProvider.notifier).completeTask(task.id); + }, + ``` + - The `dueDate` and `isOverdue` variables remain — they are used for styling the relative date text color + + + grep -n "isFuture" lib/features/home/presentation/calendar_day_list.dart lib/features/tasks/presentation/task_row.dart; echo "---"; grep -n "canComplete: true" lib/features/home/presentation/calendar_day_list.dart + + + - calendar_day_list.dart does NOT contain the string `isFuture` + - calendar_day_list.dart contains `canComplete: true` in the _buildAnimatedTaskRow call for dayTasks + - task_row.dart does NOT contain the string `isFuture` + - task_row.dart does NOT contain `? null` in the Checkbox onChanged handler + - calendar_task_row.dart is unchanged (canComplete param still exists with default true) + + All checkboxes are always enabled across calendar and task list views. No isFuture guard remains in UI code. + + + + Task 2: Update completeTask to recalculate nextDueDate from today on non-due-day completion + + lib/features/tasks/data/tasks_dao.dart + test/features/tasks/data/tasks_dao_test.dart + + + lib/features/tasks/data/tasks_dao.dart + lib/features/tasks/domain/scheduling.dart + test/features/tasks/data/tasks_dao_test.dart + + + - Test: Completing a task ON its due date recalculates nextDueDate from the original due date (existing behavior preserved) + - Test: Completing a weekly task 3 days BEFORE its due date recalculates nextDueDate from today (not original due date) — e.g., task due Friday, completed Tuesday, next due = next Tuesday + - Test: Completing a daily task on a non-due day still produces tomorrow as next due + - Test: Completing a monthly task early recalculates from today with anchor day preserved + + +**Per D-02 and D-03: When completing a task on a non-due day, recalculate nextDueDate from today.** + +In `lib/features/tasks/data/tasks_dao.dart`, method `completeTask()`: + +Change step 3 from: +```dart +// 3. Calculate next due date (from original due date, not today) +var nextDue = calculateNextDueDate( + currentDueDate: task.nextDueDate, + intervalType: task.intervalType, + intervalDays: task.intervalDays, + anchorDay: task.anchorDay, +); +``` + +To: +```dart +// 3. Calculate next due date +// If completing on the due date, use original due date as base (keeps rhythm). +// If completing on a different day (early or late), use today as base (per D-02). +final todayStart = DateTime(currentTime.year, currentTime.month, currentTime.day); +final taskDueDay = DateTime(task.nextDueDate.year, task.nextDueDate.month, task.nextDueDate.day); +final baseDate = todayStart == taskDueDay ? task.nextDueDate : todayStart; + +var nextDue = calculateNextDueDate( + currentDueDate: baseDate, + intervalType: task.intervalType, + intervalDays: task.intervalDays, + anchorDay: task.anchorDay, +); +``` + +Note: The existing `todayDateOnly` variable (line 66-70) can be replaced by `todayStart` since they are the same. Rename to avoid duplication. The catch-up step (step 4) remains unchanged — it still ensures nextDue is not in the past. + +Write 4 new test cases in `test/features/tasks/data/tasks_dao_test.dart`: +1. `completeTask on due date preserves rhythm` — weekly task due 2026-03-24, complete on 2026-03-24, next due = 2026-03-31 +2. `completeTask before due date recalculates from today` — weekly task due 2026-03-28, complete on 2026-03-24, next due = 2026-03-31 (7 days from today) +3. `completeTask daily task on non-due day` — daily task due 2026-03-26, complete on 2026-03-24, next due = 2026-03-25 (tomorrow) +4. `completeTask monthly task early preserves anchor` — monthly task due 2026-03-28 anchorDay=28, complete on 2026-03-24, next due = 2026-04-28 + + + cd /home/jean-luc-makiola/Development/projects/HouseHoldKeaper && flutter test test/features/tasks/data/tasks_dao_test.dart --reporter compact + + + - tasks_dao.dart completeTask() contains `final baseDate = todayStart == taskDueDay ? task.nextDueDate : todayStart;` + - tasks_dao.dart completeTask() passes `currentDueDate: baseDate` to calculateNextDueDate + - tasks_dao_test.dart contains test with name matching `completeTask.*before due date.*recalculates from today` + - tasks_dao_test.dart contains test with name matching `completeTask.*on due date.*preserves rhythm` + - All tests in tasks_dao_test.dart pass (exit code 0) + + completeTask() uses today as base for non-due-day completions. 4 new tests verify the behavior for on-due-day, before-due-day, daily, and monthly scenarios. All existing tests still pass. + + + + + +1. `flutter test` — all existing + new tests pass +2. `dart analyze` — zero issues +3. `grep -rn "isFuture" lib/features/home/presentation/calendar_day_list.dart lib/features/tasks/presentation/task_row.dart` — no matches +4. `grep -n "canComplete: true" lib/features/home/presentation/calendar_day_list.dart` — found in dayTasks loop + + + +- Checkboxes are always enabled on all calendar days (past, today, future) +- Checkboxes are always enabled in room task list view +- Completing a task on its due date preserves the original interval rhythm +- Completing a task on a non-due day recalculates from today +- All existing tests pass with zero regressions +- dart analyze reports zero issues + + + +After completion, create `.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-01-SUMMARY.md` + diff --git a/.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-02-PLAN.md b/.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-02-PLAN.md new file mode 100644 index 0000000..b6864ae --- /dev/null +++ b/.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-02-PLAN.md @@ -0,0 +1,566 @@ +--- +phase: 11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks +plan: 02 +type: execute +wave: 2 +depends_on: ["11-01"] +files_modified: + - lib/features/home/data/calendar_dao.dart + - lib/features/home/presentation/calendar_providers.dart + - lib/features/home/domain/calendar_models.dart + - lib/features/home/presentation/calendar_day_list.dart + - lib/features/home/presentation/calendar_task_row.dart + - test/features/home/data/calendar_dao_test.dart +autonomous: true +requirements: + - TM-03 + - TM-04 + - TM-05 + +must_haves: + truths: + - "A weekly task appears on every day within its current interval window, not just on the due date" + - "A monthly task appears on every day within its current interval window" + - "A daily task appears every day" + - "Tasks already completed in the current interval period do not reappear as pre-populated" + - "Pre-populated tasks that are not yet due have a muted/lighter visual appearance" + - "Tasks on their actual due date appear with full styling" + - "Overdue tasks keep their existing red/orange accent" + artifacts: + - path: "lib/features/home/data/calendar_dao.dart" + provides: "watchAllActiveRecurringTasks query and watchCompletionsInRange query" + contains: "watchAllActiveRecurringTasks" + - path: "lib/features/home/presentation/calendar_providers.dart" + provides: "Pre-population logic combining due-today + virtual instances + overdue" + contains: "isPrePopulated" + - path: "lib/features/home/domain/calendar_models.dart" + provides: "CalendarDayState with pre-populated task support" + contains: "prePopulatedTasks" + - path: "lib/features/home/presentation/calendar_task_row.dart" + provides: "Visual distinction for pre-populated tasks" + contains: "isPrePopulated" + - path: "lib/features/home/presentation/calendar_day_list.dart" + provides: "Renders pre-populated section with muted styling" + contains: "prePopulatedTasks" + key_links: + - from: "lib/features/home/presentation/calendar_providers.dart" + to: "lib/features/home/data/calendar_dao.dart" + via: "watchAllActiveRecurringTasks stream for pre-population source data" + pattern: "watchAllActiveRecurringTasks" + - from: "lib/features/home/presentation/calendar_providers.dart" + to: "lib/features/tasks/domain/scheduling.dart" + via: "calculateNextDueDate used to determine interval window boundaries" + pattern: "calculateNextDueDate" + - from: "lib/features/home/presentation/calendar_day_list.dart" + to: "CalendarTaskRow" + via: "isPrePopulated flag passed for visual distinction" + pattern: "isPrePopulated" +--- + + +Pre-populate recurring tasks on all applicable days within their interval window. A weekly task due Monday shows on all 7 days leading up to that Monday. Tasks already completed in the current period are hidden. Pre-populated (not-yet-due) tasks get a muted visual style. + +Purpose: Users want a consistent checklist — tasks should be visible and completable before their due date, not appear only when due. This makes the app feel like a reliable weekly/monthly checklist. +Output: Virtual task instances in provider layer, period-completion filtering, muted visual styling for upcoming tasks. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-CONTEXT.md +@.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-01-SUMMARY.md + + + + +From lib/features/home/domain/daily_plan_models.dart: +```dart +class TaskWithRoom { + final Task task; + final String roomName; + final int roomId; + const TaskWithRoom({required this.task, required this.roomName, required this.roomId}); +} +``` + +From lib/features/home/domain/calendar_models.dart: +```dart +class CalendarDayState { + final DateTime selectedDate; + final List dayTasks; + final List overdueTasks; + final int totalTaskCount; + bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty; +} +``` + +From lib/features/tasks/domain/frequency.dart: +```dart +enum IntervalType { + daily, everyNDays, weekly, biweekly, monthly, everyNMonths, quarterly, yearly, +} +``` + +From lib/features/tasks/domain/scheduling.dart: +```dart +DateTime calculateNextDueDate({ + required DateTime currentDueDate, + required IntervalType intervalType, + required int intervalDays, + int? anchorDay, +}); +``` + +From lib/features/home/data/calendar_dao.dart: +```dart +class CalendarDao extends DatabaseAccessor with _$CalendarDaoMixin { + Stream> watchTasksForDate(DateTime date); + Stream> watchOverdueTasks(DateTime referenceDate); + // + room-scoped variants +} +``` + +From lib/core/database/database.dart (Tasks table): +```dart +class Tasks extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get roomId => integer().references(Rooms, #id)(); + TextColumn get name => text().withLength(min: 1, max: 200)(); + IntColumn get intervalType => intEnum()(); + IntColumn get intervalDays => integer().withDefault(const Constant(1))(); + IntColumn get anchorDay => integer().nullable()(); + DateTimeColumn get nextDueDate => dateTime()(); + BoolColumn get isActive => BoolColumn().withDefault(const Constant(true))(); +} +``` + +From lib/core/database/database.dart (TaskCompletions table): +```dart +class TaskCompletions extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get taskId => integer().references(Tasks, #id)(); + DateTimeColumn get completedAt => dateTime()(); +} +``` + + + + + + + Task 1: Add DAO queries, update CalendarDayState model, and implement pre-population provider logic + + lib/features/home/data/calendar_dao.dart + lib/features/home/domain/calendar_models.dart + lib/features/home/presentation/calendar_providers.dart + test/features/home/data/calendar_dao_test.dart + + + lib/features/home/data/calendar_dao.dart + lib/features/home/domain/calendar_models.dart + lib/features/home/presentation/calendar_providers.dart + lib/features/tasks/domain/scheduling.dart + lib/features/tasks/domain/frequency.dart + lib/core/database/database.dart + test/features/home/data/calendar_dao_test.dart + + +**Per D-04 through D-10: Query-time virtual instances, no schema migration.** + +### Step 1: New DAO methods in calendar_dao.dart + +Add two new methods to `CalendarDao`: + +**1a. `watchAllActiveRecurringTasks()`** — Fetch ALL active tasks with their rooms (for pre-population logic): +```dart +Stream> watchAllActiveRecurringTasks() { + final query = select(tasks).join([ + innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)), + ]); + query.where(tasks.isActive.equals(true)); + query.orderBy([OrderingTerm.asc(tasks.name)]); + + return query.watch().map((rows) { + return rows.map((row) { + final task = row.readTable(tasks); + final room = row.readTable(rooms); + return TaskWithRoom(task: task, roomName: room.name, roomId: room.id); + }).toList(); + }); +} +``` + +**1b. `watchCompletionsInRange(int taskId, DateTime start, DateTime end)`** — Check if a task was completed within a date range (for period-completion filtering per D-09): +```dart +Stream> watchCompletionsInRange(int taskId, DateTime start, DateTime end) { + return (select(taskCompletions) + ..where((c) => + c.taskId.equals(taskId) & + c.completedAt.isBiggerOrEqualValue(start) & + c.completedAt.isSmallerThanValue(end))) + .watch(); +} +``` + +**1c. Room-scoped variant `watchAllActiveRecurringTasksInRoom(int roomId)`**: +Same as 1a but with additional `.where(tasks.roomId.equals(roomId))`. + +### Step 2: Update CalendarDayState in calendar_models.dart + +Add a `prePopulatedTasks` field: +```dart +class CalendarDayState { + final DateTime selectedDate; + final List dayTasks; + final List overdueTasks; + final List prePopulatedTasks; // NEW — tasks visible via pre-population + final int totalTaskCount; + + const CalendarDayState({ + required this.selectedDate, + required this.dayTasks, + required this.overdueTasks, + this.prePopulatedTasks = const [], // Default empty for backward compat + required this.totalTaskCount, + }); + + bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty && prePopulatedTasks.isEmpty; +} +``` + +### Step 3: Rewrite calendarDayProvider in calendar_providers.dart + +The provider needs to combine three streams: due-today tasks, overdue tasks, and pre-populated virtual tasks. + +Add a top-level helper function `_isInCurrentIntervalWindow` that determines if a task should appear on a given date: + +```dart +/// Determines whether [task] should appear on [selectedDate] via pre-population. +/// +/// Per D-07: A task shows on all days within the current interval window +/// leading up to its nextDueDate. For example: +/// - weekly task due Monday: shows on all 7 days (Tue-Mon) before nextDueDate +/// - monthly task due 15th: shows on all ~30 days leading up to the 15th +/// - daily task: shows every day (interval window = 1 day, always matches) +bool _isInCurrentIntervalWindow(Task task, DateTime selectedDate) { + final dueDate = DateTime(task.nextDueDate.year, task.nextDueDate.month, task.nextDueDate.day); + final selected = DateTime(selectedDate.year, selectedDate.month, selectedDate.day); + + // Task is not yet due or due today — it might be in the window + // If selected date IS the due date, it is a "due today" task (not pre-populated) + if (selected == dueDate) return false; + // If selected date is after the due date, task is overdue (handled separately) + if (selected.isAfter(dueDate)) return false; + + // Calculate the start of the current interval window: + // The previous due date = current nextDueDate minus one interval + final previousDue = _calculatePreviousDueDate(task); + final prevDay = DateTime(previousDue.year, previousDue.month, previousDue.day); + + // Selected date must be AFTER the previous due date (exclusive) + // and BEFORE the next due date (exclusive — due date itself is "dayTasks" not pre-pop) + return selected.isAfter(prevDay) && selected.isBefore(dueDate); +} +``` + +Add `_calculatePreviousDueDate` helper: +```dart +/// Reverse-calculate the previous due date by subtracting one interval. +/// This gives the start of the current interval window. +DateTime _calculatePreviousDueDate(Task task) { + switch (task.intervalType) { + case IntervalType.daily: + return task.nextDueDate.subtract(const Duration(days: 1)); + case IntervalType.everyNDays: + return task.nextDueDate.subtract(Duration(days: task.intervalDays)); + case IntervalType.weekly: + return task.nextDueDate.subtract(const Duration(days: 7)); + case IntervalType.biweekly: + return task.nextDueDate.subtract(const Duration(days: 14)); + case IntervalType.monthly: + return _subtractMonths(task.nextDueDate, 1, task.anchorDay); + case IntervalType.everyNMonths: + return _subtractMonths(task.nextDueDate, task.intervalDays, task.anchorDay); + case IntervalType.quarterly: + return _subtractMonths(task.nextDueDate, 3, task.anchorDay); + case IntervalType.yearly: + return _subtractMonths(task.nextDueDate, 12, task.anchorDay); + } +} + +DateTime _subtractMonths(DateTime date, int months, int? anchorDay) { + final targetMonth = date.month - months; + final targetYear = date.year + (targetMonth - 1) ~/ 12; + final normalizedMonth = ((targetMonth - 1) % 12) + 1; + final day = anchorDay ?? date.day; + final lastDay = DateTime(targetYear, normalizedMonth + 1, 0).day; + final clampedDay = day > lastDay ? lastDay : day; + return DateTime(targetYear, normalizedMonth, clampedDay); +} +``` + +Rewrite `calendarDayProvider` to: +1. Watch `watchAllActiveRecurringTasks()` alongside `watchTasksForDate()` +2. Filter all tasks through `_isInCurrentIntervalWindow()` to get pre-populated candidates +3. For each candidate, check if completed in current period via `watchCompletionsInRange()` (per D-09, D-10) +4. Exclude tasks already in `dayTasks` (they appear as due-today, not pre-populated) +5. Exclude tasks already in `overdueTasks` +6. Return combined state with new `prePopulatedTasks` list + +```dart +final calendarDayProvider = + StreamProvider.autoDispose((ref) { + final db = ref.watch(appDatabaseProvider); + final selectedDate = ref.watch(selectedDateProvider); + final sortOption = ref.watch(sortPreferenceProvider); + + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final isToday = selectedDate == today; + + final dayTasksStream = db.calendarDao.watchTasksForDate(selectedDate); + final allTasksStream = db.calendarDao.watchAllActiveRecurringTasks(); + + // Combine both streams + return dayTasksStream.asyncMap((dayTasks) async { + final List overdueTasks; + if (isToday) { + overdueTasks = + await db.calendarDao.watchOverdueTasks(selectedDate).first; + } else { + overdueTasks = const []; + } + + // Get all active tasks for pre-population filtering + final allTasks = await allTasksStream.first; + + // IDs of tasks already showing as due-today or overdue + final dueTodayIds = dayTasks.map((t) => t.task.id).toSet(); + final overdueIds = overdueTasks.map((t) => t.task.id).toSet(); + + // Filter for pre-populated tasks + final prePopulated = []; + for (final tw in allTasks) { + // Skip if already showing as due-today or overdue + if (dueTodayIds.contains(tw.task.id)) continue; + if (overdueIds.contains(tw.task.id)) continue; + + // Check if in current interval window + if (!_isInCurrentIntervalWindow(tw.task, selectedDate)) continue; + + // Check if already completed in current period (D-09, D-10) + final prevDue = _calculatePreviousDueDate(tw.task); + final completions = await db.calendarDao + .watchCompletionsInRange( + tw.task.id, + DateTime(prevDue.year, prevDue.month, prevDue.day), + DateTime(tw.task.nextDueDate.year, tw.task.nextDueDate.month, tw.task.nextDueDate.day).add(const Duration(days: 1)), + ) + .first; + + if (completions.isEmpty) { + prePopulated.add(tw); + } + } + + final totalTaskCount = await db.calendarDao.getTaskCount(); + + return CalendarDayState( + selectedDate: selectedDate, + dayTasks: _sortTasks(dayTasks, sortOption), + overdueTasks: overdueTasks, + prePopulatedTasks: _sortTasks(prePopulated, sortOption), + totalTaskCount: totalTaskCount, + ); + }); +}); +``` + +Apply the SAME changes to `roomCalendarDayProvider`, but use `watchAllActiveRecurringTasksInRoom(roomId)` instead of `watchAllActiveRecurringTasks()`. + +### Step 4: Add DAO tests + +Add tests to `test/features/home/data/calendar_dao_test.dart`: +1. `watchAllActiveRecurringTasks returns all active tasks` — insert 3 tasks (2 active, 1 inactive), verify 2 returned +2. `watchCompletionsInRange returns completions within date range` — insert completions at various dates, verify only in-range ones returned +3. `watchAllActiveRecurringTasksInRoom filters by room` — insert tasks in 2 rooms, verify room filter works + + + cd /home/jean-luc-makiola/Development/projects/HouseHoldKeaper && flutter test test/features/home/data/calendar_dao_test.dart --reporter compact + + + - calendar_dao.dart contains method `watchAllActiveRecurringTasks()` + - calendar_dao.dart contains method `watchCompletionsInRange(` + - calendar_dao.dart contains method `watchAllActiveRecurringTasksInRoom(` + - calendar_models.dart CalendarDayState contains field `List prePopulatedTasks` + - calendar_models.dart isEmpty getter includes `prePopulatedTasks.isEmpty` + - calendar_providers.dart contains function `_isInCurrentIntervalWindow(` + - calendar_providers.dart contains function `_calculatePreviousDueDate(` + - calendar_providers.dart calendarDayProvider passes `prePopulatedTasks:` to CalendarDayState + - calendar_providers.dart roomCalendarDayProvider passes `prePopulatedTasks:` to CalendarDayState + - calendar_dao_test.dart contains test matching `watchAllActiveRecurringTasks` + - calendar_dao_test.dart contains test matching `watchCompletionsInRange` + - All tests in calendar_dao_test.dart pass (exit code 0) + + DAO provides all-active-tasks query and completion-range query. Provider computes virtual pre-populated task instances using interval window logic. Completed-in-current-period tasks are excluded. CalendarDayState carries prePopulatedTasks. All tests pass. + + + + Task 2: Render pre-populated tasks with muted visual distinction in calendar UI + + lib/features/home/presentation/calendar_day_list.dart + lib/features/home/presentation/calendar_task_row.dart + + + lib/features/home/presentation/calendar_day_list.dart + lib/features/home/presentation/calendar_task_row.dart + lib/features/home/domain/calendar_models.dart + + +**Per D-11, D-12, D-13: Visual distinction for pre-populated tasks.** + +### Step 1: Add `isPrePopulated` prop to CalendarTaskRow + +In `calendar_task_row.dart`, add an `isPrePopulated` parameter: +```dart +class CalendarTaskRow extends StatelessWidget { + const CalendarTaskRow({ + super.key, + required this.taskWithRoom, + required this.onCompleted, + this.isOverdue = false, + this.showRoomTag = true, + this.canComplete = true, + this.isPrePopulated = false, // NEW + }); + + final bool isPrePopulated; // NEW +``` + +In the `build` method, apply muted styling when `isPrePopulated` is true: +- Wrap the entire `ListTile` in an `Opacity` widget with `opacity: isPrePopulated ? 0.55 : 1.0` +- This gives pre-populated tasks a subtle "upcoming, not yet due" appearance per D-11 +- Overdue tasks (`isOverdue: true`) are never pre-populated, so no conflict with D-13 +- Due-today tasks are not pre-populated either, so full styling is preserved per D-12 + +```dart +@override +Widget build(BuildContext context) { + final theme = Theme.of(context); + final task = taskWithRoom.task; + + final tile = ListTile( + // ... existing ListTile code unchanged ... + ); + + return isPrePopulated ? Opacity(opacity: 0.55, child: tile) : tile; +} +``` + +### Step 2: Render pre-populated tasks in CalendarDayList + +In `calendar_day_list.dart`, update `_buildTaskList` to include pre-populated tasks: + +After the day tasks loop and before the `return ListView(children: items);`, add: +```dart +// Pre-populated tasks section (upcoming tasks within interval window). +if (state.prePopulatedTasks.isNotEmpty) { + items.add(_buildSectionHeader('Demnächst', theme, + color: theme.colorScheme.onSurface.withValues(alpha: 0.5))); + for (final tw in state.prePopulatedTasks) { + items.add(_buildAnimatedTaskRow( + tw, + isOverdue: false, + showRoomTag: showRoomTag, + canComplete: true, + isPrePopulated: true, + )); + } +} +``` + +Update `_buildAnimatedTaskRow` to accept and pass `isPrePopulated`: +```dart +Widget _buildAnimatedTaskRow( + TaskWithRoom tw, { + required bool isOverdue, + required bool showRoomTag, + required bool canComplete, + bool isPrePopulated = false, +}) { + // ... existing completing animation check ... + + return CalendarTaskRow( + key: ValueKey('task-${tw.task.id}'), + taskWithRoom: tw, + isOverdue: isOverdue, + showRoomTag: showRoomTag, + canComplete: canComplete, + isPrePopulated: isPrePopulated, + onCompleted: () => _onTaskCompleted(tw.task.id), + ); +} +``` + +Also update `_CompletingTaskRow` to pass `isPrePopulated: false` (completing tasks are always full-styled). + +### Step 3: Update celebration state logic + +In `_buildContent`, update the celebration check to also verify prePopulatedTasks is empty: +```dart +if (isToday && state.dayTasks.isEmpty && state.overdueTasks.isEmpty && state.prePopulatedTasks.isEmpty && state.totalTaskCount > 0) { + return _buildCelebration(l10n, theme); +} +``` + +### Step 4: Section header "Demnächst" (German for "Coming up") + +The section header text `'Demnächst'` is hardcoded for now. If a localization key is preferred, add `calendarPrePopulatedSection` to the l10n strings. For consistency with the existing pattern of using `l10n.dailyPlanSectionOverdue` for the overdue header, add a new key. However, the CONTEXT.md does not mandate localization changes, so inline German string is acceptable. + + + cd /home/jean-luc-makiola/Development/projects/HouseHoldKeaper && dart analyze lib/features/home/presentation/calendar_day_list.dart lib/features/home/presentation/calendar_task_row.dart + + + - calendar_task_row.dart contains `final bool isPrePopulated;` + - calendar_task_row.dart contains `this.isPrePopulated = false` + - calendar_task_row.dart contains `Opacity(opacity: 0.55` or `opacity: isPrePopulated ? 0.55 : 1.0` + - calendar_day_list.dart contains `state.prePopulatedTasks.isNotEmpty` + - calendar_day_list.dart contains string `'Demnächst'` + - calendar_day_list.dart contains `isPrePopulated: true` in the pre-populated tasks loop + - calendar_day_list.dart _buildAnimatedTaskRow signature contains `bool isPrePopulated` + - dart analyze reports zero issues for both files + + Pre-populated tasks render below day tasks with "Demnächst" section header. They have 0.55 opacity for visual distinction. Due-today tasks have full styling. Overdue tasks keep coral accent. All checkboxes functional. dart analyze clean. + + + + + +1. `flutter test` — ALL tests pass (existing + new) +2. `dart analyze` — zero issues across entire project +3. `grep -n "prePopulatedTasks" lib/features/home/domain/calendar_models.dart lib/features/home/presentation/calendar_providers.dart lib/features/home/presentation/calendar_day_list.dart` — found in all three files +4. `grep -n "isPrePopulated" lib/features/home/presentation/calendar_task_row.dart lib/features/home/presentation/calendar_day_list.dart` — found in both files +5. `grep -n "watchAllActiveRecurringTasks" lib/features/home/data/calendar_dao.dart` — method exists +6. `grep -n "watchCompletionsInRange" lib/features/home/data/calendar_dao.dart` — method exists + + + +- Weekly tasks appear on all 7 days leading up to their due date +- Monthly tasks appear on all days within their current month interval +- Daily tasks appear every day (since their interval window is 1 day, they are always "due today" and show as dayTasks, not pre-populated) +- Completing a pre-populated task triggers normal completeTask flow and the task disappears from remaining days in the period +- Pre-populated tasks have a muted 0.55 opacity appearance +- Due-today tasks show with full styling +- Overdue tasks keep coral color +- All tests pass, dart analyze clean + + + +After completion, create `.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-02-SUMMARY.md` +