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 |
|
|
true |
|
|
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.mdFrom 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()();
}
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:
- Watch
watchAllActiveRecurringTasks()alongsidewatchTasksForDate() - Filter all tasks through
_isInCurrentIntervalWindow()to get pre-populated candidates - For each candidate, check if completed in current period via
watchCompletionsInRange()(per D-09, D-10) - Exclude tasks already in
dayTasks(they appear as due-today, not pre-populated) - Exclude tasks already in
overdueTasks - Return combined state with new
prePopulatedTaskslist
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:
watchAllActiveRecurringTasks returns all active tasks— insert 3 tasks (2 active, 1 inactive), verify 2 returnedwatchCompletionsInRange returns completions within date range— insert completions at various dates, verify only in-range ones returnedwatchAllActiveRecurringTasksInRoom 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.
- calendar_dao.dart contains method
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
ListTilein anOpacitywidget withopacity: 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.
<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>