docs(05-calendar-strip): create phase plan

This commit is contained in:
2026-03-16 21:14:59 +01:00
parent fe7ba21061
commit 31d4ef879b
3 changed files with 583 additions and 2 deletions

View File

@@ -0,0 +1,262 @@
---
phase: 05-calendar-strip
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- lib/features/home/data/calendar_dao.dart
- lib/features/home/data/calendar_dao.g.dart
- lib/features/home/domain/calendar_models.dart
- lib/features/home/presentation/calendar_providers.dart
- lib/core/database/database.dart
- lib/core/database/database.g.dart
- lib/l10n/app_de.arb
- lib/l10n/app_localizations_de.dart
- lib/l10n/app_localizations.dart
- test/features/home/data/calendar_dao_test.dart
autonomous: true
requirements:
- CAL-02
- CAL-05
must_haves:
truths:
- "Querying tasks for any arbitrary date returns exactly the tasks whose nextDueDate falls on that day"
- "Querying overdue tasks for today returns all tasks whose nextDueDate is strictly before today"
- "Querying a future date returns only tasks due that day, no overdue carry-over"
- "CalendarState model holds selectedDate, overdue tasks, and day tasks as separate lists"
- "Localization strings for calendar UI exist in ARB and generated files"
artifacts:
- path: "lib/features/home/data/calendar_dao.dart"
provides: "Date-parameterized task queries"
exports: ["CalendarDao"]
- path: "lib/features/home/domain/calendar_models.dart"
provides: "CalendarState and reuse of TaskWithRoom"
exports: ["CalendarState"]
- path: "lib/features/home/presentation/calendar_providers.dart"
provides: "Riverpod provider for calendar state"
exports: ["calendarProvider", "selectedDateProvider"]
- path: "test/features/home/data/calendar_dao_test.dart"
provides: "DAO unit tests"
min_lines: 50
key_links:
- from: "lib/features/home/data/calendar_dao.dart"
to: "lib/core/database/database.dart"
via: "DAO registered in @DriftDatabase annotation"
pattern: "CalendarDao"
- from: "lib/features/home/presentation/calendar_providers.dart"
to: "lib/features/home/data/calendar_dao.dart"
via: "Provider reads CalendarDao from AppDatabase"
pattern: "db\\.calendarDao"
---
<objective>
Create the data layer, domain models, Riverpod providers, and localization strings for the calendar strip feature.
Purpose: The calendar strip UI (Plan 02) needs a data foundation that can answer "what tasks are due on date X?" and "what tasks are overdue relative to today?" without the old overdue/today/tomorrow bucketing. This plan builds that foundation and tests it.
Output: CalendarDao with date-parameterized queries, CalendarState model, Riverpod providers (selectedDateProvider + calendarProvider), new l10n strings, DAO 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/05-calendar-strip/5-CONTEXT.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From lib/core/database/database.dart:
```dart
// Tables: Rooms, Tasks, TaskCompletions
// Existing DAOs: RoomsDao, TasksDao, DailyPlanDao
// CalendarDao must be added to the @DriftDatabase annotation daos list
// and imported at the top of database.dart
@DriftDatabase(
tables: [Rooms, Tasks, TaskCompletions],
daos: [RoomsDao, TasksDao, DailyPlanDao], // ADD CalendarDao here
)
class AppDatabase extends _$AppDatabase { ... }
```
From lib/features/home/domain/daily_plan_models.dart:
```dart
class TaskWithRoom {
final Task task;
final String roomName;
final int roomId;
const TaskWithRoom({required this.task, required this.roomName, required this.roomId});
}
```
From lib/features/home/presentation/daily_plan_providers.dart:
```dart
// Pattern to follow: StreamProvider.autoDispose, manual (not @riverpod)
// because of drift's generated Task type
final dailyPlanProvider = StreamProvider.autoDispose<DailyPlanState>((ref) {
final db = ref.watch(appDatabaseProvider);
...
});
```
From lib/features/home/data/daily_plan_dao.dart:
```dart
// Pattern: @DriftAccessor with tables, extends DatabaseAccessor<AppDatabase>
// Uses query.watch() for reactive streams
@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
class DailyPlanDao extends DatabaseAccessor<AppDatabase> with _$DailyPlanDaoMixin { ... }
```
From lib/core/providers/database_provider.dart:
```dart
// appDatabaseProvider gives access to the database singleton
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create CalendarDao with date-parameterized queries and tests</name>
<files>
lib/features/home/data/calendar_dao.dart,
lib/core/database/database.dart,
test/features/home/data/calendar_dao_test.dart
</files>
<behavior>
- watchTasksForDate(date): returns tasks whose nextDueDate falls on the given calendar day (same year/month/day), joined with room name, sorted by task name alphabetically
- watchOverdueTasks(referenceDate): returns tasks whose nextDueDate is strictly before referenceDate (start of day), joined with room name, sorted by nextDueDate ascending
- watchTasksForDate for a date with no tasks returns empty list
- watchOverdueTasks returns empty when no tasks are overdue
- watchOverdueTasks does NOT include tasks due on the referenceDate itself
- watchTasksForDate for a past date returns only tasks originally due that day (does NOT include overdue carry-over)
</behavior>
<action>
1. Create `lib/features/home/data/calendar_dao.dart`:
- Class `CalendarDao` extends `DatabaseAccessor<AppDatabase>` with `_$CalendarDaoMixin`
- Annotated `@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])`
- `part 'calendar_dao.g.dart';`
- Method `Stream<List<TaskWithRoom>> watchTasksForDate(DateTime date)`:
Compute startOfDay and endOfDay (startOfDay + 1 day). Join tasks with rooms. Filter `tasks.nextDueDate >= startOfDay AND tasks.nextDueDate < endOfDay`. Order by `tasks.name` ascending. Map to `TaskWithRoom`.
- Method `Stream<List<TaskWithRoom>> watchOverdueTasks(DateTime referenceDate)`:
Compute startOfReferenceDay. Join tasks with rooms. Filter `tasks.nextDueDate < startOfReferenceDay`. Order by `tasks.nextDueDate` ascending. Map to `TaskWithRoom`.
- Import `daily_plan_models.dart` for `TaskWithRoom` (reuse, don't duplicate).
2. Register CalendarDao in `lib/core/database/database.dart`:
- Add import: `import '../../features/home/data/calendar_dao.dart';`
- Add `CalendarDao` to the `daos:` list in `@DriftDatabase`
3. Run `dart run build_runner build --delete-conflicting-outputs` to generate `calendar_dao.g.dart` and updated `database.g.dart`.
4. Write `test/features/home/data/calendar_dao_test.dart` following the pattern in `test/features/home/data/daily_plan_dao_test.dart`:
- Use in-memory database: `AppDatabase(NativeDatabase.memory())`
- Create test rooms in setUp
- Test group for watchTasksForDate:
- Empty when no tasks
- Returns only tasks due on the queried date (not before, not after)
- Returns tasks from multiple rooms
- Sorted alphabetically by name
- Test group for watchOverdueTasks:
- Empty when no overdue tasks
- Returns tasks due before reference date
- Does NOT include tasks due ON the reference date
- Sorted by nextDueDate ascending
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/home/data/calendar_dao_test.dart</automated>
</verify>
<done>CalendarDao registered in AppDatabase, both query methods return correct results for arbitrary dates, all DAO tests pass</done>
</task>
<task type="auto">
<name>Task 2: Create CalendarState model, Riverpod providers, and localization strings</name>
<files>
lib/features/home/domain/calendar_models.dart,
lib/features/home/presentation/calendar_providers.dart,
lib/l10n/app_de.arb
</files>
<action>
1. Create `lib/features/home/domain/calendar_models.dart`:
```dart
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
/// State for the calendar day view: tasks for the selected date + overdue tasks.
class CalendarDayState {
final DateTime selectedDate;
final List<TaskWithRoom> dayTasks;
final List<TaskWithRoom> overdueTasks;
const CalendarDayState({
required this.selectedDate,
required this.dayTasks,
required this.overdueTasks,
});
/// True when viewing today and all tasks (day + overdue) have been completed
/// (lists are empty but completions exist). Determined by the UI layer.
bool get isEmpty => dayTasks.isEmpty && overdueTasks.isEmpty;
}
```
2. Create `lib/features/home/presentation/calendar_providers.dart`:
- Import Riverpod, database_provider, calendar_dao, calendar_models, daily_plan_models
- `final selectedDateProvider = StateProvider<DateTime>((ref) { final now = DateTime.now(); return DateTime(now.year, now.month, now.day); });`
This is NOT autoDispose -- the selected date persists as long as the app is alive (resets on restart naturally).
- `final calendarDayProvider = StreamProvider.autoDispose<CalendarDayState>((ref) { ... });`
Manual definition (not @riverpod) following dailyPlanProvider pattern.
Reads `selectedDateProvider` to get the current date.
Reads `appDatabaseProvider` to get the DB.
Determines if selectedDate is today: `isToday = selectedDate == DateTime(now.year, now.month, now.day)`.
Determines if selectedDate is in the future: `isFuture = selectedDate.isAfter(today)`.
Watches `db.calendarDao.watchTasksForDate(selectedDate)`.
For overdue: if `isToday`, also watch `db.calendarDao.watchOverdueTasks(selectedDate)`.
If viewing a past date or future date, overdueTasks = empty.
Per user decision: "When viewing past days: show what was due that day. When viewing future days: show only tasks due that day, no overdue carry-over."
Combine both streams using `Rx.combineLatest2` or simply use `asyncMap` on the day tasks stream and fetch overdue as a secondary query.
Implementation approach: Use the dayTasks stream as the primary, and inside asyncMap call the overdue stream's `.first` when isToday. This keeps it simple and follows the existing `dailyPlanProvider` pattern of `stream.asyncMap()`.
3. Add new l10n strings to `lib/l10n/app_de.arb` (add before the closing `}`):
- `"calendarNoTasks": "Keine Aufgaben"` — shown when a day has no tasks at all
- `"calendarAllDone": "Alles erledigt!"` — celebration when all tasks for a day are done
- `"calendarOverdueSection": "Uberfaellig"` — No, reuse existing `dailyPlanSectionOverdue` ("Uberfaellig") for the overdue section header
- `"calendarTodayButton": "Heute"` — floating today button label
Actually, we can reuse `dailyPlanSectionOverdue` for the overdue header, `dailyPlanNoTasks` for no-tasks-at-all, and `dailyPlanAllClearTitle`/`dailyPlanAllClearMessage` for celebration. The only truly new string needed is for the Today button:
- Add `"calendarTodayButton": "Heute"` to the ARB file
4. Run `flutter gen-l10n` to regenerate localization files.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos</automated>
</verify>
<done>CalendarDayState model exists with selectedDate/dayTasks/overdueTasks fields. selectedDateProvider and calendarDayProvider are defined. calendarDayProvider returns overdue tasks only when viewing today. New l10n string "calendarTodayButton" exists. No analysis errors.</done>
</task>
</tasks>
<verification>
- `flutter test test/features/home/data/calendar_dao_test.dart` — all DAO tests pass
- `flutter analyze --no-fatal-infos` — no errors in new or modified files
- `flutter test` — full test suite still passes (existing tests not broken by database.dart changes)
</verification>
<success_criteria>
- CalendarDao is registered in AppDatabase and has two working query methods
- CalendarDayState model correctly separates day tasks from overdue tasks
- calendarDayProvider returns overdue only for today, not for past/future dates
- All existing tests still pass after database.dart modification
- New DAO tests cover core query behaviors
</success_criteria>
<output>
After completion, create `.planning/phases/05-calendar-strip/05-01-SUMMARY.md`
</output>