278 lines
13 KiB
Markdown
278 lines
13 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
|
|
|
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<IntervalType>()();
|
|
IntColumn get intervalDays => integer().withDefault(const Constant(1))();
|
|
IntColumn get anchorDay => integer().nullable()();
|
|
IntColumn get effortLevel => intEnum<EffortLevel>()();
|
|
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<List<Task>, int>((ref, roomId) {
|
|
final db = ref.watch(appDatabaseProvider);
|
|
return db.tasksDao.watchTasksInRoom(roomId);
|
|
});
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: DailyPlanDao with cross-room join query and completion count</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<behavior>
|
|
- 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
|
|
</behavior>
|
|
<action>
|
|
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<TaskWithRoom> overdueTasks`, `final List<TaskWithRoom> todayTasks`, `final List<TaskWithRoom> 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<AppDatabase> with _$DailyPlanDaoMixin`
|
|
- `Stream<List<TaskWithRoom>> watchAllTasksWithRoomName()`: innerJoin tasks with rooms on rooms.id.equalsExp(tasks.roomId), orderBy nextDueDate asc, map rows using readTable(tasks) and readTable(rooms)
|
|
- `Stream<int> 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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/home/data/daily_plan_dao_test.dart</automated>
|
|
</verify>
|
|
<done>
|
|
- 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
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Daily plan provider with date categorization, progress tracking, and localization keys</name>
|
|
<files>
|
|
lib/features/home/presentation/daily_plan_providers.dart,
|
|
lib/l10n/app_de.arb
|
|
</files>
|
|
<action>
|
|
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<DailyPlanState>` (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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze lib/features/home/ lib/l10n/ && flutter test</automated>
|
|
</verify>
|
|
<done>
|
|
- 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
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-daily-plan-and-cleanliness/03-01-SUMMARY.md`
|
|
</output>
|