Files

23 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks 02 execute 2
11-01
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
true
TM-03
TM-04
TM-05
truths artifacts key_links
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
path provides contains
lib/features/home/data/calendar_dao.dart watchAllActiveRecurringTasks query and watchCompletionsInRange query watchAllActiveRecurringTasks
path provides contains
lib/features/home/presentation/calendar_providers.dart Pre-population logic combining due-today + virtual instances + overdue isPrePopulated
path provides contains
lib/features/home/domain/calendar_models.dart CalendarDayState with pre-populated task support prePopulatedTasks
path provides contains
lib/features/home/presentation/calendar_task_row.dart Visual distinction for pre-populated tasks isPrePopulated
path provides contains
lib/features/home/presentation/calendar_day_list.dart Renders pre-populated section with muted styling prePopulatedTasks
from to via pattern
lib/features/home/presentation/calendar_providers.dart lib/features/home/data/calendar_dao.dart watchAllActiveRecurringTasks stream for pre-population source data watchAllActiveRecurringTasks
from to via pattern
lib/features/home/presentation/calendar_providers.dart lib/features/tasks/domain/scheduling.dart calculateNextDueDate used to determine interval window boundaries calculateNextDueDate
from to via pattern
lib/features/home/presentation/calendar_day_list.dart CalendarTaskRow isPrePopulated flag passed for visual distinction 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

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

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:

class CalendarDayState {
  final DateTime selectedDate;
  final List<TaskWithRoom> dayTasks;
  final List<TaskWithRoom> overdueTasks;
  final int totalTaskCount;
  bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty;
}

From lib/features/tasks/domain/frequency.dart:

enum IntervalType {
  daily, everyNDays, weekly, biweekly, monthly, everyNMonths, quarterly, yearly,
}

From lib/features/tasks/domain/scheduling.dart:

DateTime calculateNextDueDate({
  required DateTime currentDueDate,
  required IntervalType intervalType,
  required int intervalDays,
  int? anchorDay,
});

From lib/features/home/data/calendar_dao.dart:

class CalendarDao extends DatabaseAccessor<AppDatabase> with _$CalendarDaoMixin {
  Stream<List<TaskWithRoom>> watchTasksForDate(DateTime date);
  Stream<List<TaskWithRoom>> watchOverdueTasks(DateTime referenceDate);
  // + room-scoped variants
}

From lib/core/database/database.dart (Tasks table):

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<IntervalType>()();
  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):

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

Stream<List<TaskWithRoom>> 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):

Stream<List<TaskCompletion>> 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:

class CalendarDayState {
  final DateTime selectedDate;
  final List<TaskWithRoom> dayTasks;
  final List<TaskWithRoom> overdueTasks;
  final List<TaskWithRoom> 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:

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

/// 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
final calendarDayProvider =
    StreamProvider.autoDispose<CalendarDayState>((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<TaskWithRoom> 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 = <TaskWithRoom>[];
    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 <acceptance_criteria>
    • 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<TaskWithRoom> 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) </acceptance_criteria> 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:

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

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

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:

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 <acceptance_criteria> - 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 </acceptance_criteria> 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/11-issue-3-tasks-management-allow-task-checking-anytime-and-pre-populate-recurring-tasks/11-02-SUMMARY.md`