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 with _$TasksDaoMixin { TasksDao(super.attachedDatabase); /// Watch tasks in a room sorted by nextDueDate ascending. Stream> watchTasksInRoom(int roomId) { return (select(tasks) ..where((t) => t.roomId.equals(roomId)) ..orderBy([(t) => OrderingTerm.asc(t.nextDueDate)])) .watch(); } /// Insert a new task. Returns the auto-generated id. Future insertTask(TasksCompanion task) => into(tasks).insert(task); /// Update an existing task. Returns true if a row was updated. Future updateTask(Task task) => update(tasks).replace(task); /// Delete a task and its completions. Future 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. Next due is calculated /// from the original due date (not completion date) to keep rhythm stable. /// If the calculated next due is in the past, catch-up advances to present. /// /// [now] parameter allows injection of current time for testing. Future 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 (from original due date, not today) var nextDue = calculateNextDueDate( currentDueDate: task.nextDueDate, 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, 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> 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). Future 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))) .get(); return taskList.where((task) { final dueDate = DateTime( task.nextDueDate.year, task.nextDueDate.month, task.nextDueDate.day, ); return dueDate.isBefore(todayDateOnly); }).length; } }