Files
HouseHoldKeaper/lib/features/tasks/data/tasks_dao.dart
Jean-Luc Makiola c5ab052f9e 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
2026-03-24 09:47:49 +01:00

133 lines
4.8 KiB
Dart

import 'package:drift/drift.dart';
import '../../../core/database/database.dart';
import '../domain/scheduling.dart';
part 'tasks_dao.g.dart';
@DriftAccessor(tables: [Tasks, TaskCompletions])
class TasksDao extends DatabaseAccessor<AppDatabase> with _$TasksDaoMixin {
TasksDao(super.attachedDatabase);
/// Watch tasks in a room sorted by nextDueDate ascending.
/// Only returns active tasks (isActive = true).
Stream<List<Task>> watchTasksInRoom(int roomId) {
return (select(tasks)
..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true))
..orderBy([(t) => OrderingTerm.asc(t.nextDueDate)]))
.watch();
}
/// Insert a new task. Returns the auto-generated id.
Future<int> insertTask(TasksCompanion task) => into(tasks).insert(task);
/// Update an existing task. Returns true if a row was updated.
Future<bool> updateTask(Task task) => update(tasks).replace(task);
/// Delete a task and its completions.
Future<void> deleteTask(int taskId) {
return transaction(() async {
await (delete(taskCompletions)..where((c) => c.taskId.equals(taskId)))
.go();
await (delete(tasks)..where((t) => t.id.equals(taskId))).go();
});
}
/// Mark a task as done: records completion and calculates next due date.
///
/// 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.
Future<void> completeTask(int taskId, {DateTime? now}) {
return transaction(() async {
// 1. Get current task
final task =
await (select(tasks)..where((t) => t.id.equals(taskId))).getSingle();
final currentTime = now ?? DateTime.now();
// 2. Record completion
await into(taskCompletions).insert(TaskCompletionsCompanion.insert(
taskId: taskId,
completedAt: currentTime,
));
// 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: baseDate,
intervalType: task.intervalType,
intervalDays: task.intervalDays,
anchorDay: task.anchorDay,
);
// 4. Catch up if next due is still in the past
nextDue = catchUpToPresent(
nextDue: nextDue,
today: todayStart,
intervalType: task.intervalType,
intervalDays: task.intervalDays,
anchorDay: task.anchorDay,
);
// 5. Update task with new due date
await (update(tasks)..where((t) => t.id.equals(taskId)))
.write(TasksCompanion(nextDueDate: Value(nextDue)));
});
}
/// Watch all completions for a task, newest first.
Stream<List<TaskCompletion>> watchCompletionsForTask(int taskId) {
return (select(taskCompletions)
..where((c) => c.taskId.equals(taskId))
..orderBy([(c) => OrderingTerm.desc(c.completedAt)]))
.watch();
}
/// Count overdue tasks in a room (nextDueDate before today).
/// Only counts active tasks (isActive = true).
Future<int> getOverdueTaskCount(int roomId, {DateTime? today}) async {
final now = today ?? DateTime.now();
final todayDateOnly = DateTime(now.year, now.month, now.day);
final taskList = await (select(tasks)
..where((t) => t.roomId.equals(roomId) & t.isActive.equals(true)))
.get();
return taskList.where((task) {
final dueDate = DateTime(
task.nextDueDate.year,
task.nextDueDate.month,
task.nextDueDate.day,
);
return dueDate.isBefore(todayDateOnly);
}).length;
}
/// Soft-delete a task by setting isActive to false.
/// The task and its completions remain in the database.
Future<void> softDeleteTask(int taskId) {
return (update(tasks)..where((t) => t.id.equals(taskId)))
.write(const TasksCompanion(isActive: Value(false)));
}
/// Count completions for a task.
Future<int> getCompletionCount(int taskId) async {
final count = taskCompletions.id.count();
final query = selectOnly(taskCompletions)
..addColumns([count])
..where(taskCompletions.taskId.equals(taskId));
final result = await query.getSingle();
return result.read(count) ?? 0;
}
}