Merge branch 'worktree-agent-aca389e2' into Develop
Some checks failed
CI / ci (push) Failing after 4m28s
Some checks failed
CI / ci (push) Failing after 4m28s
This commit is contained in:
@@ -240,9 +240,6 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
|||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
ThemeData theme,
|
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 showRoomTag = widget.roomId == null;
|
||||||
|
|
||||||
final items = <Widget>[];
|
final items = <Widget>[];
|
||||||
@@ -268,7 +265,7 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
|||||||
tw,
|
tw,
|
||||||
isOverdue: false,
|
isOverdue: false,
|
||||||
showRoomTag: showRoomTag,
|
showRoomTag: showRoomTag,
|
||||||
canComplete: !isFuture,
|
canComplete: true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
|
|||||||
|
|
||||||
/// Mark a task as done: records completion and calculates next due date.
|
/// Mark a task as done: records completion and calculates next due date.
|
||||||
///
|
///
|
||||||
/// Uses scheduling utility for date calculation. Next due is calculated
|
/// Uses scheduling utility for date calculation. If completing on the due
|
||||||
/// from the original due date (not completion date) to keep rhythm stable.
|
/// 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.
|
/// If the calculated next due is in the past, catch-up advances to present.
|
||||||
///
|
///
|
||||||
/// [now] parameter allows injection of current time for testing.
|
/// [now] parameter allows injection of current time for testing.
|
||||||
@@ -54,23 +56,24 @@ class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
|
|||||||
completedAt: currentTime,
|
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(
|
var nextDue = calculateNextDueDate(
|
||||||
currentDueDate: task.nextDueDate,
|
currentDueDate: baseDate,
|
||||||
intervalType: task.intervalType,
|
intervalType: task.intervalType,
|
||||||
intervalDays: task.intervalDays,
|
intervalDays: task.intervalDays,
|
||||||
anchorDay: task.anchorDay,
|
anchorDay: task.anchorDay,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Catch up if next due is still in the past
|
// 4. Catch up if next due is still in the past
|
||||||
final todayDateOnly = DateTime(
|
|
||||||
currentTime.year,
|
|
||||||
currentTime.month,
|
|
||||||
currentTime.day,
|
|
||||||
);
|
|
||||||
nextDue = catchUpToPresent(
|
nextDue = catchUpToPresent(
|
||||||
nextDue: nextDue,
|
nextDue: nextDue,
|
||||||
today: todayDateOnly,
|
today: todayStart,
|
||||||
intervalType: task.intervalType,
|
intervalType: task.intervalType,
|
||||||
intervalDays: task.intervalDays,
|
intervalDays: task.intervalDays,
|
||||||
anchorDay: task.anchorDay,
|
anchorDay: task.anchorDay,
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class TaskRow extends ConsumerWidget {
|
|||||||
task.nextDueDate.day,
|
task.nextDueDate.day,
|
||||||
);
|
);
|
||||||
final isOverdue = dueDate.isBefore(today);
|
final isOverdue = dueDate.isBefore(today);
|
||||||
final isFuture = dueDate.isAfter(today);
|
|
||||||
|
|
||||||
// Format relative due date in German
|
// Format relative due date in German
|
||||||
final relativeDateText = formatRelativeDate(task.nextDueDate, now);
|
final relativeDateText = formatRelativeDate(task.nextDueDate, now);
|
||||||
@@ -57,12 +56,10 @@ class TaskRow extends ConsumerWidget {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Checkbox(
|
leading: Checkbox(
|
||||||
value: false, // Always unchecked -- completion is immediate + reschedule
|
value: false, // Always unchecked -- completion is immediate + reschedule
|
||||||
onChanged: isFuture
|
onChanged: (_) {
|
||||||
? null // Future tasks cannot be completed yet
|
// Mark done immediately (optimistic UI, no undo per user decision)
|
||||||
: (_) {
|
ref.read(taskActionsProvider.notifier).completeTask(task.id);
|
||||||
// Mark done immediately (optimistic UI, no undo per user decision)
|
},
|
||||||
ref.read(taskActionsProvider.notifier).completeTask(task.id);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
task.name,
|
task.name,
|
||||||
|
|||||||
@@ -317,6 +317,79 @@ void main() {
|
|||||||
expect(overdueCount, 1);
|
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 {
|
test('hard deleteTask still removes task and its completions', () async {
|
||||||
final id = await db.tasksDao.insertTask(
|
final id = await db.tasksDao.insertTask(
|
||||||
TasksCompanion.insert(
|
TasksCompanion.insert(
|
||||||
|
|||||||
Reference in New Issue
Block a user