From b00806a597a45d2638ba7f75b41e7c5d2eae6af9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 24 Mar 2026 09:44:58 +0100 Subject: [PATCH 1/3] feat(11-01): remove checkbox-disable restrictions for future tasks - Remove isFuture guard in calendar_day_list.dart, pass canComplete: true always - Remove isFuture check in task_row.dart, always enable Checkbox.onChanged - calendar_task_row.dart unchanged (canComplete param already defaults to true) --- lib/features/home/presentation/calendar_day_list.dart | 5 +---- lib/features/tasks/presentation/task_row.dart | 11 ++++------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/features/home/presentation/calendar_day_list.dart b/lib/features/home/presentation/calendar_day_list.dart index 134cf9b..58626dd 100644 --- a/lib/features/home/presentation/calendar_day_list.dart +++ b/lib/features/home/presentation/calendar_day_list.dart @@ -240,9 +240,6 @@ class _CalendarDayListState extends ConsumerState { AppLocalizations l10n, ThemeData theme, ) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - final isFuture = state.selectedDate.isAfter(today); final showRoomTag = widget.roomId == null; final items = []; @@ -268,7 +265,7 @@ class _CalendarDayListState extends ConsumerState { tw, isOverdue: false, showRoomTag: showRoomTag, - canComplete: !isFuture, + canComplete: true, )); } diff --git a/lib/features/tasks/presentation/task_row.dart b/lib/features/tasks/presentation/task_row.dart index e375d12..4e3665b 100644 --- a/lib/features/tasks/presentation/task_row.dart +++ b/lib/features/tasks/presentation/task_row.dart @@ -42,7 +42,6 @@ class TaskRow extends ConsumerWidget { task.nextDueDate.day, ); final isOverdue = dueDate.isBefore(today); - final isFuture = dueDate.isAfter(today); // Format relative due date in German final relativeDateText = formatRelativeDate(task.nextDueDate, now); @@ -57,12 +56,10 @@ class TaskRow extends ConsumerWidget { return ListTile( leading: Checkbox( value: false, // Always unchecked -- completion is immediate + reschedule - onChanged: isFuture - ? null // Future tasks cannot be completed yet - : (_) { - // Mark done immediately (optimistic UI, no undo per user decision) - ref.read(taskActionsProvider.notifier).completeTask(task.id); - }, + onChanged: (_) { + // Mark done immediately (optimistic UI, no undo per user decision) + ref.read(taskActionsProvider.notifier).completeTask(task.id); + }, ), title: Text( task.name, From 3398acab333ee06b5ea4b34625d2b3715755c2cb Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 24 Mar 2026 09:47:43 +0100 Subject: [PATCH 2/3] test(11-01): add failing tests for non-due-day completion recalculation - Test: completeTask on due date preserves rhythm (weekly task) - Test: completeTask before due date recalculates from today - Test: completeTask daily task on non-due day recalculates from today - Test: completeTask monthly task early preserves anchor day --- test/features/tasks/data/tasks_dao_test.dart | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/features/tasks/data/tasks_dao_test.dart b/test/features/tasks/data/tasks_dao_test.dart index fa13da1..f5729e6 100644 --- a/test/features/tasks/data/tasks_dao_test.dart +++ b/test/features/tasks/data/tasks_dao_test.dart @@ -317,6 +317,79 @@ void main() { expect(overdueCount, 1); }); + test('completeTask on due date preserves rhythm', () async { + // Weekly task due 2026-03-24, completed on 2026-03-24: next due = 2026-03-31 + final id = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Weekly On Due Day', + intervalType: IntervalType.weekly, + effortLevel: EffortLevel.medium, + nextDueDate: DateTime(2026, 3, 24), + ), + ); + + await db.tasksDao.completeTask(id, now: DateTime(2026, 3, 24)); + + final tasks = await db.tasksDao.watchTasksInRoom(roomId).first; + expect(tasks.first.nextDueDate, DateTime(2026, 3, 31)); + }); + + test('completeTask before due date recalculates from today', () async { + // Weekly task due 2026-03-28, completed on 2026-03-24 (Tuesday): next due = 2026-03-31 (7 days from today) + final id = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Weekly Before Due Day', + intervalType: IntervalType.weekly, + effortLevel: EffortLevel.medium, + nextDueDate: DateTime(2026, 3, 28), + ), + ); + + await db.tasksDao.completeTask(id, now: DateTime(2026, 3, 24)); + + final tasks = await db.tasksDao.watchTasksInRoom(roomId).first; + expect(tasks.first.nextDueDate, DateTime(2026, 3, 31)); + }); + + test('completeTask daily task on non-due day recalculates from today', () async { + // Daily task due 2026-03-26, completed on 2026-03-24: next due = 2026-03-25 (tomorrow) + final id = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Daily Non-Due Day', + intervalType: IntervalType.daily, + effortLevel: EffortLevel.low, + nextDueDate: DateTime(2026, 3, 26), + ), + ); + + await db.tasksDao.completeTask(id, now: DateTime(2026, 3, 24)); + + final tasks = await db.tasksDao.watchTasksInRoom(roomId).first; + expect(tasks.first.nextDueDate, DateTime(2026, 3, 25)); + }); + + test('completeTask monthly task early preserves anchor', () async { + // Monthly task due 2026-03-28 anchorDay=28, completed on 2026-03-24: next due = 2026-04-28 + final id = await db.tasksDao.insertTask( + TasksCompanion.insert( + roomId: roomId, + name: 'Monthly Early Completion', + intervalType: IntervalType.monthly, + effortLevel: EffortLevel.high, + nextDueDate: DateTime(2026, 3, 28), + anchorDay: const Value(28), + ), + ); + + await db.tasksDao.completeTask(id, now: DateTime(2026, 3, 24)); + + final tasks = await db.tasksDao.watchTasksInRoom(roomId).first; + expect(tasks.first.nextDueDate, DateTime(2026, 4, 28)); + }); + test('hard deleteTask still removes task and its completions', () async { final id = await db.tasksDao.insertTask( TasksCompanion.insert( From c5ab052f9e6c659f5f0b51260f0fb584a0669269 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 24 Mar 2026 09:47:49 +0100 Subject: [PATCH 3/3] feat(11-01): recalculate nextDueDate from today on non-due-day completion - When completing on due date: use original due date as base (preserves rhythm) - When completing on different day: use today as base (per D-02) - Replace todayDateOnly with todayStart used for both base calculation and catch-up - Update doc comment to reflect new behavior --- lib/features/tasks/data/tasks_dao.dart | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/features/tasks/data/tasks_dao.dart b/lib/features/tasks/data/tasks_dao.dart index c9d96c1..4829236 100644 --- a/lib/features/tasks/data/tasks_dao.dart +++ b/lib/features/tasks/data/tasks_dao.dart @@ -35,8 +35,10 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { /// Mark a task as done: records completion and calculates next due date. /// - /// Uses scheduling utility for date calculation. Next due is calculated - /// from the original due date (not completion date) to keep rhythm stable. + /// Uses scheduling utility for date calculation. If completing on the due + /// date, next due is calculated from the original due date (keeps rhythm). + /// If completing on a different day (early or late), next due is calculated + /// from today (per D-02: matches user mental model "I did it now, schedule next from now"). /// If the calculated next due is in the past, catch-up advances to present. /// /// [now] parameter allows injection of current time for testing. @@ -54,23 +56,24 @@ class TasksDao extends DatabaseAccessor with _$TasksDaoMixin { completedAt: currentTime, )); - // 3. Calculate next due date (from original due date, not today) + // 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: task.nextDueDate, + currentDueDate: baseDate, intervalType: task.intervalType, intervalDays: task.intervalDays, anchorDay: task.anchorDay, ); // 4. Catch up if next due is still in the past - final todayDateOnly = DateTime( - currentTime.year, - currentTime.month, - currentTime.day, - ); nextDue = catchUpToPresent( nextDue: nextDue, - today: todayDateOnly, + today: todayStart, intervalType: task.intervalType, intervalDays: task.intervalDays, anchorDay: task.anchorDay,