--- 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`