docs(05-calendar-strip): create phase plan
This commit is contained in:
262
.planning/phases/05-calendar-strip/05-01-PLAN.md
Normal file
262
.planning/phases/05-calendar-strip/05-01-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user