Files
HouseHoldKeaper/lib/features/home/data/daily_plan_dao.dart
Jean-Luc Makiola 878767138c feat(04-01): Android config, NotificationService, DAO queries, timezone init, ARB strings
- Add flutter_local_notifications ^21.0.0, timezone ^0.11.0, flutter_timezone ^1.0.8 to pubspec.yaml
- Set compileSdk=35, enable core library desugaring in build.gradle.kts
- Add POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED permissions and boot receivers to AndroidManifest.xml
- Create NotificationService singleton with initialize, requestPermission, scheduleDailyNotification, cancelAll
- Add getOverdueAndTodayTaskCount and getOverdueTaskCount one-shot queries to DailyPlanDao
- Initialize timezone and NotificationService in main.dart before runApp
- Add 7 notification-related ARB strings to app_de.arb
- All 72 existing tests pass
2026-03-16 14:52:29 +01:00

75 lines
2.7 KiB
Dart

import 'package:drift/drift.dart';
import '../../../core/database/database.dart';
import '../domain/daily_plan_models.dart';
part 'daily_plan_dao.g.dart';
@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
class DailyPlanDao extends DatabaseAccessor<AppDatabase>
with _$DailyPlanDaoMixin {
DailyPlanDao(super.attachedDatabase);
/// Watch all tasks joined with room name, sorted by nextDueDate ascending.
/// Includes ALL tasks (overdue, today, future) -- filtering is done in the
/// provider layer to avoid multiple queries.
Stream<List<TaskWithRoom>> watchAllTasksWithRoomName() {
final query = select(tasks).join([
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
]);
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();
});
}
/// One-shot count of overdue + today tasks (for notification body).
Future<int> getOverdueAndTodayTaskCount({DateTime? today}) async {
final now = today ?? DateTime.now();
final endOfToday = DateTime(now.year, now.month, now.day + 1);
final result = await (selectOnly(tasks)
..addColumns([tasks.id.count()])
..where(tasks.nextDueDate.isSmallerThanValue(endOfToday)))
.getSingle();
return result.read(tasks.id.count()) ?? 0;
}
/// One-shot count of overdue tasks only (for notification body split).
Future<int> getOverdueTaskCount({DateTime? today}) async {
final now = today ?? DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final result = await (selectOnly(tasks)
..addColumns([tasks.id.count()])
..where(tasks.nextDueDate.isSmallerThanValue(startOfToday)))
.getSingle();
return result.read(tasks.id.count()) ?? 0;
}
/// Count task completions recorded today.
/// Uses customSelect with readsFrom for proper stream invalidation.
Stream<int> watchCompletionsToday({DateTime? today}) {
final now = today ?? DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
final endOfDay = startOfDay.add(const Duration(days: 1));
return customSelect(
'SELECT COUNT(*) AS c FROM task_completions '
'WHERE completed_at >= ? AND completed_at < ?',
variables: [
Variable(startOfDay.millisecondsSinceEpoch ~/ 1000),
Variable(endOfDay.millisecondsSinceEpoch ~/ 1000),
],
readsFrom: {taskCompletions},
).watchSingle().map((row) => row.read<int>('c'));
}
}