Files
HouseHoldKeaper/lib/features/home/data/calendar_dao.dart
Jean-Luc Makiola b2f14dcd97 feat(08-01): add isActive filters to CalendarDao, DailyPlanDao, RoomsDao
- CalendarDao: filter all 6 task queries (watchTasksForDate,
  watchTasksForDateInRoom, watchOverdueTasks, watchOverdueTasksInRoom,
  getTaskCount, getTaskCountInRoom) by isActive=true
- DailyPlanDao: filter all 3 queries (watchAllTasksWithRoomName,
  getOverdueAndTodayTaskCount, getOverdueTaskCount) by isActive=true
- RoomsDao: filter watchRoomWithStats task query by isActive=true
- Update migration test: add schema_v3.dart, test v1->v3 and v2->v3 paths
- Update database_test schemaVersion assertion to expect 3
- Fix test helpers in home_screen_test and task_list_screen_test to pass isActive=true
2026-03-18 20:56:34 +01:00

169 lines
5.4 KiB
Dart

import 'package:drift/drift.dart';
import '../../../core/database/database.dart';
import '../domain/daily_plan_models.dart';
part 'calendar_dao.g.dart';
/// DAO for calendar-based task queries.
///
/// Provides date-parameterized queries to answer:
/// - "What tasks are due on date X?"
/// - "What tasks are overdue relative to today?"
@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
class CalendarDao extends DatabaseAccessor<AppDatabase>
with _$CalendarDaoMixin {
CalendarDao(super.attachedDatabase);
/// Watch tasks whose [nextDueDate] falls on the given calendar day.
///
/// Returns tasks sorted alphabetically by name.
/// Does NOT include overdue carry-over — only tasks originally due on [date].
Stream<List<TaskWithRoom>> watchTasksForDate(DateTime date) {
final startOfDay = DateTime(date.year, date.month, date.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
final query = select(tasks).join([
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
]);
query.where(
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
tasks.nextDueDate.isSmallerThanValue(endOfDay) &
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();
});
}
/// Returns the total count of active tasks across all rooms and dates.
///
/// Used by the UI to distinguish first-run empty state from celebration state.
Future<int> getTaskCount() async {
final countExp = tasks.id.count();
final query = selectOnly(tasks)
..addColumns([countExp])
..where(tasks.isActive.equals(true));
final result = await query.getSingle();
return result.read(countExp) ?? 0;
}
/// Watch tasks due on [date] within a specific [roomId].
///
/// Same as [watchTasksForDate] but filtered to a single room.
Stream<List<TaskWithRoom>> watchTasksForDateInRoom(
DateTime date, int roomId) {
final startOfDay = DateTime(date.year, date.month, date.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
final query = select(tasks).join([
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
]);
query.where(
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
tasks.nextDueDate.isSmallerThanValue(endOfDay) &
tasks.roomId.equals(roomId) &
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();
});
}
/// Watch tasks whose [nextDueDate] is strictly before [referenceDate].
///
/// Returns tasks sorted by [nextDueDate] ascending (oldest first).
/// Does NOT include tasks due on [referenceDate] itself.
Stream<List<TaskWithRoom>> watchOverdueTasks(DateTime referenceDate) {
final startOfReferenceDay = DateTime(
referenceDate.year,
referenceDate.month,
referenceDate.day,
);
final query = select(tasks).join([
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
]);
query.where(
tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) &
tasks.isActive.equals(true),
);
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
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();
});
}
/// Watch overdue tasks (before [referenceDate]) within a specific [roomId].
///
/// Same as [watchOverdueTasks] but filtered to a single room.
Stream<List<TaskWithRoom>> watchOverdueTasksInRoom(
DateTime referenceDate, int roomId) {
final startOfReferenceDay = DateTime(
referenceDate.year,
referenceDate.month,
referenceDate.day,
);
final query = select(tasks).join([
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
]);
query.where(
tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) &
tasks.roomId.equals(roomId) &
tasks.isActive.equals(true),
);
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
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();
});
}
/// Total active task count within a specific room.
///
/// Used to distinguish first-run empty state from celebration state
/// in the room calendar view.
Future<int> getTaskCountInRoom(int roomId) async {
final countExp = tasks.id.count();
final query = selectOnly(tasks)
..addColumns([countExp])
..where(tasks.roomId.equals(roomId) & tasks.isActive.equals(true));
final result = await query.getSingle();
return result.read(countExp) ?? 0;
}
}