--- phase: 03-daily-plan-and-cleanliness plan: 01 type: execute wave: 1 depends_on: [] files_modified: - lib/features/home/data/daily_plan_dao.dart - lib/features/home/data/daily_plan_dao.g.dart - lib/features/home/domain/daily_plan_models.dart - lib/features/home/presentation/daily_plan_providers.dart - lib/core/database/database.dart - lib/core/database/database.g.dart - lib/l10n/app_de.arb - test/features/home/data/daily_plan_dao_test.dart autonomous: true requirements: - PLAN-01 - PLAN-02 - PLAN-03 - PLAN-05 must_haves: truths: - "DailyPlanDao.watchAllTasksWithRoomName() returns tasks joined with room name, sorted by nextDueDate ascending" - "DailyPlanDao.watchCompletionsToday() returns count of completions recorded today" - "dailyPlanProvider categorizes tasks into overdue, today, and tomorrow sections" - "Progress total = remaining overdue + remaining today + completedTodayCount (stable denominator)" - "Localization keys for daily plan sections and progress text exist in app_de.arb" artifacts: - path: "lib/features/home/data/daily_plan_dao.dart" provides: "Cross-room join query and today's completion count" exports: ["DailyPlanDao", "TaskWithRoom"] - path: "lib/features/home/domain/daily_plan_models.dart" provides: "DailyPlanState data class for categorized daily plan data" exports: ["DailyPlanState"] - path: "lib/features/home/presentation/daily_plan_providers.dart" provides: "Riverpod provider combining task stream and completion stream" exports: ["dailyPlanProvider"] - path: "test/features/home/data/daily_plan_dao_test.dart" provides: "Unit tests for cross-room query, date categorization, completion count" min_lines: 80 key_links: - from: "lib/features/home/data/daily_plan_dao.dart" to: "lib/core/database/database.dart" via: "@DriftAccessor registration" pattern: "DailyPlanDao" - from: "lib/features/home/presentation/daily_plan_providers.dart" to: "lib/features/home/data/daily_plan_dao.dart" via: "db.dailyPlanDao.watchAllTasksWithRoomName()" pattern: "dailyPlanDao" --- Build the data and provider layers for the daily plan feature: a Drift DAO with cross-room join query, a DailyPlanState model with overdue/today/tomorrow categorization, a Riverpod provider combining task and completion streams, localization keys, and unit tests. Purpose: Provides the reactive data foundation that the daily plan UI (Plan 02) will consume. Separated from UI to keep each plan at ~50% context. Output: DailyPlanDao with join query, DailyPlanState model, dailyPlanProvider, localization keys, and passing unit tests. @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-daily-plan-and-cleanliness/3-CONTEXT.md @.planning/phases/03-daily-plan-and-cleanliness/03-RESEARCH.md @lib/core/database/database.dart @lib/features/tasks/data/tasks_dao.dart @lib/features/tasks/presentation/task_providers.dart @lib/core/providers/database_provider.dart @test/features/tasks/data/tasks_dao_test.dart From lib/core/database/database.dart: ```dart class Rooms extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text().withLength(min: 1, max: 100)(); TextColumn get iconName => text()(); IntColumn get sortOrder => integer().withDefault(const Constant(0))(); DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); } class Tasks extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get roomId => integer().references(Rooms, #id)(); TextColumn get name => text().withLength(min: 1, max: 200)(); TextColumn get description => text().nullable()(); IntColumn get intervalType => intEnum()(); IntColumn get intervalDays => integer().withDefault(const Constant(1))(); IntColumn get anchorDay => integer().nullable()(); IntColumn get effortLevel => intEnum()(); DateTimeColumn get nextDueDate => dateTime()(); DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); } class TaskCompletions extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get taskId => integer().references(Tasks, #id)(); DateTimeColumn get completedAt => dateTime()(); } @DriftDatabase( tables: [Rooms, Tasks, TaskCompletions], daos: [RoomsDao, TasksDao], ) class AppDatabase extends _$AppDatabase { ... } ``` From lib/core/providers/database_provider.dart: ```dart @Riverpod(keepAlive: true) AppDatabase appDatabase(Ref ref) { ... } ``` From lib/features/tasks/presentation/task_providers.dart: ```dart // Manual StreamProvider.family pattern (used because riverpod_generator // has trouble with drift's generated Task type) final tasksInRoomProvider = StreamProvider.family.autoDispose, int>((ref, roomId) { final db = ref.watch(appDatabaseProvider); return db.tasksDao.watchTasksInRoom(roomId); }); ``` Task 1: DailyPlanDao with cross-room join query and completion count lib/features/home/data/daily_plan_dao.dart, lib/features/home/data/daily_plan_dao.g.dart, lib/features/home/domain/daily_plan_models.dart, lib/core/database/database.dart, lib/core/database/database.g.dart, test/features/home/data/daily_plan_dao_test.dart - watchAllTasksWithRoomName returns empty list when no tasks exist - watchAllTasksWithRoomName returns tasks with correct room name from join - watchAllTasksWithRoomName returns tasks sorted by nextDueDate ascending - watchAllTasksWithRoomName returns tasks from multiple rooms with correct room name pairing - watchCompletionsToday returns 0 when no completions exist - watchCompletionsToday returns correct count of completions recorded today - watchCompletionsToday does not count completions from yesterday 1. Create `lib/features/home/domain/daily_plan_models.dart` with: - `TaskWithRoom` class: `final Task task`, `final String roomName`, `final int roomId`, const constructor - `DailyPlanState` class: `final List overdueTasks`, `final List todayTasks`, `final List tomorrowTasks`, `final int completedTodayCount`, `final int totalTodayCount`, const constructor 2. Create `lib/features/home/data/daily_plan_dao.dart` with: - `@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])` - `class DailyPlanDao extends DatabaseAccessor with _$DailyPlanDaoMixin` - `Stream> watchAllTasksWithRoomName()`: innerJoin tasks with rooms on rooms.id.equalsExp(tasks.roomId), orderBy nextDueDate asc, map rows using readTable(tasks) and readTable(rooms) - `Stream watchCompletionsToday({DateTime? today})`: count TaskCompletions where completedAt >= startOfDay AND completedAt < endOfDay. Use customSelect with SQL COUNT(*) and readsFrom: {taskCompletions} for proper stream invalidation 3. Register DailyPlanDao in `lib/core/database/database.dart`: - Add import for daily_plan_dao.dart - Add `DailyPlanDao` to `@DriftDatabase(daos: [...])` list - Run `dart run build_runner build --delete-conflicting-outputs` to regenerate database.g.dart and daily_plan_dao.g.dart 4. Create `test/features/home/data/daily_plan_dao_test.dart`: - Follow existing tasks_dao_test.dart pattern: `AppDatabase(NativeDatabase.memory())`, setUp/tearDown - Create 2 rooms and insert tasks with different due dates across them - Test all behaviors listed above - Use stream.first for single-emission testing (same pattern as existing tests) IMPORTANT: The customSelect approach for watchCompletionsToday must use `readsFrom: {taskCompletions}` so Drift knows which table to watch for stream invalidation. Without this, the stream won't re-fire on new completions. cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/home/data/daily_plan_dao_test.dart - DailyPlanDao registered in AppDatabase, code generation passes - watchAllTasksWithRoomName returns tasks joined with room name, sorted by due date - watchCompletionsToday returns accurate count of today's completions - All unit tests pass - TaskWithRoom and DailyPlanState models defined with correct fields Task 2: Daily plan provider with date categorization, progress tracking, and localization keys lib/features/home/presentation/daily_plan_providers.dart, lib/l10n/app_de.arb 1. Create `lib/features/home/presentation/daily_plan_providers.dart` with: - Import daily_plan_models.dart, database_provider.dart - Define `dailyPlanProvider` as a manual `StreamProvider.autoDispose` (NOT using @riverpod, same pattern as tasksInRoomProvider because drift Task type causes riverpod_generator issues) - Inside provider: `ref.watch(appDatabaseProvider)` to get db - Watch `db.dailyPlanDao.watchAllTasksWithRoomName()` stream - Use `.asyncMap()` on the task stream to: a. Get completions today count via `db.dailyPlanDao.watchCompletionsToday().first` b. Compute `today = DateTime(now.year, now.month, now.day)`, `tomorrow = today + 1 day`, `dayAfterTomorrow = tomorrow + 1 day` c. Partition tasks into: overdue (dueDate < today), todayList (today <= dueDate < tomorrow), tomorrowList (tomorrow <= dueDate < dayAfterTomorrow) d. Compute totalTodayCount = overdue.length + todayList.length + completedTodayCount e. Return DailyPlanState with all fields CRITICAL for progress accuracy: totalTodayCount includes completedTodayCount so the denominator stays stable as tasks are completed. Without this, completing a task would shrink the total (since the task moves to a future due date), making progress appear to go backward. 2. Add localization keys to `lib/l10n/app_de.arb` (add these AFTER existing keys, before the closing brace): ```json "dailyPlanProgress": "{completed} von {total} erledigt", "@dailyPlanProgress": { "placeholders": { "completed": { "type": "int" }, "total": { "type": "int" } } }, "dailyPlanSectionOverdue": "\u00dcberf\u00e4llig", "dailyPlanSectionToday": "Heute", "dailyPlanSectionUpcoming": "Demn\u00e4chst", "dailyPlanUpcomingCount": "Demn\u00e4chst ({count})", "@dailyPlanUpcomingCount": { "placeholders": { "count": { "type": "int" } } }, "dailyPlanAllClearTitle": "Alles erledigt! \ud83c\udf1f", "dailyPlanAllClearMessage": "Keine Aufgaben f\u00fcr heute. Genie\u00dfe den Moment!", "dailyPlanNoOverdue": "Keine \u00fcberf\u00e4lligen Aufgaben", "dailyPlanNoTasks": "Noch keine Aufgaben angelegt" ``` Note: Use Unicode escapes for umlauts in ARB keys (same pattern as existing keys). The "all clear" title includes a star emoji per the established playful German tone from Phase 1. 3. Run `flutter gen-l10n` (or `flutter pub get` which triggers it) to regenerate localization classes. 4. Verify `dart analyze` passes clean on all new files. cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze lib/features/home/ lib/l10n/ && flutter test - dailyPlanProvider defined as manual StreamProvider.autoDispose returning DailyPlanState - Tasks correctly categorized into overdue (before today), today (today), tomorrow (next day) - Progress total is stable: remaining overdue + remaining today + completedTodayCount - All 10 new localization keys present in app_de.arb and code-generated without errors - dart analyze clean, full test suite passes - `flutter test test/features/home/data/daily_plan_dao_test.dart` -- all DAO tests pass - `dart analyze lib/features/home/` -- no analysis errors - `flutter test` -- full suite still passes (no regressions) - DailyPlanDao registered in AppDatabase daos list - dailyPlanProvider compiles and references DailyPlanDao correctly - DailyPlanDao.watchAllTasksWithRoomName() returns reactive stream of tasks joined with room names - DailyPlanDao.watchCompletionsToday() returns reactive count of today's completions - dailyPlanProvider categorizes tasks into overdue/today/tomorrow with stable progress tracking - All localization keys for daily plan UI are defined - All existing tests still pass (no regressions from database.dart changes) After completion, create `.planning/phases/03-daily-plan-and-cleanliness/03-01-SUMMARY.md`