docs(phase-2): research rooms and tasks domain
This commit is contained in:
804
.planning/phases/02-rooms-and-tasks/02-RESEARCH.md
Normal file
804
.planning/phases/02-rooms-and-tasks/02-RESEARCH.md
Normal file
@@ -0,0 +1,804 @@
|
|||||||
|
# Phase 2: Rooms and Tasks - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-03-15
|
||||||
|
**Domain:** Flutter CRUD features with Drift ORM, Riverpod 3 state management, scheduling logic
|
||||||
|
**Confidence:** HIGH
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 2 is the first feature-heavy phase of HouseHoldKeaper. It transforms the empty shell from Phase 1 into a functional app where users create rooms, add tasks with recurrence schedules, mark tasks done, and see overdue indicators. The core technical domains are: (1) Drift table definitions with schema migration from v1 to v2, (2) Riverpod 3 providers wrapping Drift stream queries for reactive UI, (3) date arithmetic for calendar-anchored and day-count recurrence scheduling, (4) drag-and-drop reorderable grid for room cards, and (5) a template data system for German-language task presets.
|
||||||
|
|
||||||
|
The existing codebase establishes clear patterns: `@riverpod` code generation with `Ref` (not old generated ref types), `AppDatabase` with `NativeDatabase.memory()` for tests, GoRouter `StatefulShellRoute` with nested routes, Material 3 theming via `ColorScheme.fromSeed`, and ARB-based German localization. Phase 2 builds directly on these foundations.
|
||||||
|
|
||||||
|
**Primary recommendation:** Define three Drift tables (Rooms, Tasks, TaskCompletions), create focused DAOs, expose them through Riverpod stream providers, and implement scheduling logic as a pure Dart utility with comprehensive unit tests. Use `flutter_reorderable_grid_view` for the room card drag-and-drop grid. Store templates as Dart constants (not JSON assets) for type safety and simplicity.
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- **Room cards & layout**: 2-column grid layout on the Rooms screen. Each card shows room icon, room name, count of due/overdue tasks, thin cleanliness progress bar. No next-task preview or total task count on cards. Cleanliness indicator is a thin horizontal progress bar at bottom of card, fill color shifts green to yellow to red based on ratio of on-time to overdue tasks. Icon picker is a curated grid of ~20-30 hand-picked household Material Icons in a bottom sheet. Cards support drag-and-drop reorder (ROOM-04). Delete room with confirmation dialog that warns about cascade deletion of all tasks (ROOM-03).
|
||||||
|
- **Task completion & overdue**: Leading checkbox on each task row to mark done -- tap to toggle. No swipe gesture. Tapping the task row (not the checkbox) opens task detail/edit. Overdue visual: due date text turns warm red/coral color. Rest of row stays normal. No undo on completion -- immediate and final. Records timestamp, auto-calculates next due date. Task row info: task name, relative due date (e.g. "Heute", "in 3 Tagen", "Uberfaellig"), and frequency label (e.g. "Woechentlich", "Alle 3 Tage"). No effort indicator or description preview on list view. Tasks within a room sorted by due date (default sort order, TASK-06).
|
||||||
|
- **Template selection flow**: Post-creation prompt: user creates a room first (name + icon), then gets prompted "Aufgaben aus Vorlagen hinzufuegen?" with template selection. Room type is optional -- used only to determine which templates to suggest. Not stored as a permanent field. If no matching room type is detected, no template prompt appears. All templates unchecked by default -- user explicitly checks what they want. No pre-selection. Users can create fully custom rooms (name + icon only) with no template prompt if no room type matches. Templates cover all 14 room types from TMPL-02. Templates are bundled in the app as static data (German language).
|
||||||
|
- **Scheduling & recurrence**: Two interval categories: day-count intervals (daily, every N days, weekly, biweekly) add N days from due date, pure arithmetic. Calendar-anchored intervals (monthly, quarterly, every N months, yearly) anchor to original day-of-month with clamping to last day of month but remembering the anchor. Next due calculated from original due date, not completion date. Catch-up on very late completion: keep adding intervals until next due is today or in the future. Custom intervals: user picks a number + unit (Tage/Wochen/Monate). Preset intervals from TASK-04. All due dates stored as date-only (calendar day).
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Room creation form layout (full screen vs bottom sheet vs dialog)
|
||||||
|
- Task creation/edit form layout and field ordering
|
||||||
|
- Exact Material Icons chosen for the curated icon picker set
|
||||||
|
- Drag-and-drop reorder implementation approach (ReorderableListView vs custom)
|
||||||
|
- Delete confirmation dialog design
|
||||||
|
- Animation on task completion (checkbox fill, row transition)
|
||||||
|
- Template data structure and storage format (Dart constants vs JSON asset)
|
||||||
|
- Exact color values for overdue red/coral (within the sage & stone palette)
|
||||||
|
- Empty state design for rooms with no tasks (following Phase 1 playful tone)
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
None -- discussion stayed within phase scope
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
<phase_requirements>
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|-----------------|
|
||||||
|
| ROOM-01 | Create a room with name and icon from curated Material Icons set | Drift Rooms table, DAO insert, icon picker bottom sheet with curated icons |
|
||||||
|
| ROOM-02 | Edit a room's name and icon | DAO update method, room edit form reusing creation form |
|
||||||
|
| ROOM-03 | Delete a room with confirmation (cascades to associated tasks) | DAO delete with transaction for cascade, confirmation dialog pattern |
|
||||||
|
| ROOM-04 | Reorder rooms via drag-and-drop on rooms screen | `flutter_reorderable_grid_view` package, `sortOrder` column in Rooms table |
|
||||||
|
| ROOM-05 | View all rooms as cards showing name, icon, due task count, cleanliness indicator | Drift stream query joining Rooms with Tasks for computed fields, 2-column GridView |
|
||||||
|
| TASK-01 | Create a task within a room with name, description, frequency interval, effort level | Drift Tasks table, DAO insert, task creation form, frequency/effort enums |
|
||||||
|
| TASK-02 | Edit a task's name, description, frequency interval, effort level | DAO update method, task edit form reusing creation form |
|
||||||
|
| TASK-03 | Delete a task with confirmation | DAO delete, confirmation dialog |
|
||||||
|
| TASK-04 | Set frequency interval from preset list or custom (every N days) | `FrequencyInterval` enum/model, custom interval UI with number + unit picker |
|
||||||
|
| TASK-05 | Set effort level (low/medium/high) on a task | `EffortLevel` enum stored via `intEnum` in Drift |
|
||||||
|
| TASK-06 | Sort tasks within a room by due date (default) | Drift `orderBy` on `nextDueDate` column in DAO query |
|
||||||
|
| TASK-07 | Mark task done via tap, records completion, auto-calculates next due date | TaskCompletions table, scheduling utility, DAO transaction for insert completion + update task |
|
||||||
|
| TASK-08 | Overdue tasks visually highlighted with distinct color on room cards and task lists | Drift query comparing `nextDueDate` with today, coral/warm red color in theme |
|
||||||
|
| TMPL-01 | Select from bundled German-language task templates when creating a room | Dart constant maps of room type to template list, template selection bottom sheet |
|
||||||
|
| TMPL-02 | Preset room types with templates for 14 room types | Static template data covering all 14 room types in German |
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core (already in project)
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| drift | 2.31.0 | Type-safe SQLite ORM | Already established in Phase 1, provides table definitions, DAOs, stream queries, migrations |
|
||||||
|
| drift_dev | 2.31.0 | Drift code generation | Generates table companions, database classes |
|
||||||
|
| flutter_riverpod | 3.3.1 | State management | Already established, `@riverpod` code generation with `Ref` |
|
||||||
|
| riverpod_generator | 4.0.3 | Provider code generation | Generates providers from `@riverpod` annotations |
|
||||||
|
| go_router | 17.1.0 | Declarative routing | Already established with `StatefulShellRoute`, add nested routes for rooms |
|
||||||
|
| build_runner | 2.4.0 | Code generation runner | Already in project for drift + riverpod |
|
||||||
|
|
||||||
|
### New Dependencies
|
||||||
|
| Library | Version | Purpose | When to Use |
|
||||||
|
|---------|---------|---------|-------------|
|
||||||
|
| flutter_reorderable_grid_view | ^5.6.0 | Drag-and-drop reorderable grid | Room cards drag-and-drop reorder (ROOM-04) |
|
||||||
|
|
||||||
|
### Alternatives Considered
|
||||||
|
| Instead of | Could Use | Tradeoff |
|
||||||
|
|------------|-----------|----------|
|
||||||
|
| `flutter_reorderable_grid_view` | `reorderable_grid_view` | `flutter_reorderable_grid_view` is more actively maintained with recent v5.6 overhaul, better animation support |
|
||||||
|
| `flutter_reorderable_grid_view` | Built-in `ReorderableListView` | Only supports lists, not grids. Room cards need a 2-column grid layout per user decision |
|
||||||
|
| External date package (`date_kit`) | Custom scheduling utility | Scheduling rules are highly specific (anchor memory, catch-up logic). A custom utility with ~50 lines is simpler and fully testable vs pulling in a dependency for one function |
|
||||||
|
| JSON asset for templates | Dart constants | Dart constants give type safety, IDE support, and zero parsing overhead. JSON would add asset loading complexity for no benefit |
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
```bash
|
||||||
|
flutter pub add flutter_reorderable_grid_view
|
||||||
|
```
|
||||||
|
|
||||||
|
No other new dependencies needed. All core libraries are already installed.
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### Recommended Project Structure
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
core/
|
||||||
|
database/
|
||||||
|
database.dart # Add Rooms, Tasks, TaskCompletions tables
|
||||||
|
database.g.dart # Regenerated with new tables
|
||||||
|
providers/
|
||||||
|
database_provider.dart # Unchanged (existing)
|
||||||
|
router/
|
||||||
|
router.dart # Add nested room routes
|
||||||
|
theme/
|
||||||
|
app_theme.dart # Unchanged (existing)
|
||||||
|
features/
|
||||||
|
rooms/
|
||||||
|
data/
|
||||||
|
rooms_dao.dart # Room CRUD + stream queries
|
||||||
|
rooms_dao.g.dart
|
||||||
|
domain/
|
||||||
|
room_icons.dart # Curated Material Icons list
|
||||||
|
presentation/
|
||||||
|
rooms_screen.dart # Replace placeholder with room grid
|
||||||
|
room_card.dart # Individual room card widget
|
||||||
|
room_form_screen.dart # Room create/edit form
|
||||||
|
icon_picker_sheet.dart # Bottom sheet icon picker
|
||||||
|
room_providers.dart # Riverpod providers for rooms
|
||||||
|
room_providers.g.dart
|
||||||
|
tasks/
|
||||||
|
data/
|
||||||
|
tasks_dao.dart # Task CRUD + stream queries
|
||||||
|
tasks_dao.g.dart
|
||||||
|
domain/
|
||||||
|
scheduling.dart # Pure Dart scheduling logic
|
||||||
|
frequency.dart # Frequency interval model
|
||||||
|
effort_level.dart # Effort level enum
|
||||||
|
relative_date.dart # German relative date formatter
|
||||||
|
presentation/
|
||||||
|
task_list_screen.dart # Tasks within a room
|
||||||
|
task_row.dart # Individual task row widget
|
||||||
|
task_form_screen.dart # Task create/edit form
|
||||||
|
task_providers.dart # Riverpod providers for tasks
|
||||||
|
task_providers.g.dart
|
||||||
|
templates/
|
||||||
|
data/
|
||||||
|
task_templates.dart # Static Dart constant template data
|
||||||
|
presentation/
|
||||||
|
template_picker_sheet.dart # Template selection bottom sheet
|
||||||
|
l10n/
|
||||||
|
app_de.arb # Add ~40 new localization keys
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 1: Drift Table Definition with Enums
|
||||||
|
**What:** Define tables as Dart classes extending `Table`, use `intEnum<T>()` for enum columns, `references()` for foreign keys.
|
||||||
|
**When to use:** All database table definitions.
|
||||||
|
**Example:**
|
||||||
|
```dart
|
||||||
|
// Source: drift.simonbinder.eu/dart_api/tables/
|
||||||
|
enum EffortLevel { low, medium, high }
|
||||||
|
|
||||||
|
// Frequency is stored as two columns: intervalType (enum) + intervalDays (int)
|
||||||
|
enum IntervalType { daily, everyNDays, weekly, biweekly, monthly, everyNMonths, quarterly, yearly }
|
||||||
|
|
||||||
|
class Rooms extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get name => text().withLength(min: 1, max: 100)();
|
||||||
|
TextColumn get iconName => text()(); // Material Icon name as string
|
||||||
|
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))(); // For custom intervals
|
||||||
|
IntColumn get anchorDay => integer().nullable()(); // For calendar-anchored: original day-of-month
|
||||||
|
IntColumn get effortLevel => intEnum<EffortLevel>()();
|
||||||
|
DateTimeColumn get nextDueDate => dateTime()(); // Date-only, stored as midnight
|
||||||
|
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()(); // Timestamp of completion
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Drift DAO with Stream Queries
|
||||||
|
**What:** DAOs group related database operations. Stream queries via `.watch()` provide reactive data to the UI.
|
||||||
|
**When to use:** All data access for rooms and tasks.
|
||||||
|
**Example:**
|
||||||
|
```dart
|
||||||
|
// Source: drift.simonbinder.eu/dart_api/daos/
|
||||||
|
@DriftAccessor(tables: [Rooms, Tasks])
|
||||||
|
class RoomsDao extends DatabaseAccessor<AppDatabase> with _$RoomsDaoMixin {
|
||||||
|
RoomsDao(super.attachedDatabase);
|
||||||
|
|
||||||
|
// Watch all rooms ordered by sortOrder
|
||||||
|
Stream<List<Room>> watchAllRooms() {
|
||||||
|
return (select(rooms)..orderBy([(r) => OrderingTerm.asc(r.sortOrder)])).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a new room
|
||||||
|
Future<int> insertRoom(RoomsCompanion room) => into(rooms).insert(room);
|
||||||
|
|
||||||
|
// Update room
|
||||||
|
Future<bool> updateRoom(Room room) => update(rooms).replace(room);
|
||||||
|
|
||||||
|
// Delete room with cascade (transaction)
|
||||||
|
Future<void> deleteRoom(int roomId) {
|
||||||
|
return transaction(() async {
|
||||||
|
// Delete completions for tasks in this room
|
||||||
|
final taskIds = await (select(tasks)..where((t) => t.roomId.equals(roomId)))
|
||||||
|
.map((t) => t.id).get();
|
||||||
|
for (final taskId in taskIds) {
|
||||||
|
await (delete(taskCompletions)..where((c) => c.taskId.equals(taskId))).go();
|
||||||
|
}
|
||||||
|
// Delete tasks
|
||||||
|
await (delete(tasks)..where((t) => t.roomId.equals(roomId))).go();
|
||||||
|
// Delete room
|
||||||
|
await (delete(rooms)..where((r) => r.id.equals(roomId))).go();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder rooms
|
||||||
|
Future<void> reorderRooms(List<int> roomIds) {
|
||||||
|
return transaction(() async {
|
||||||
|
for (var i = 0; i < roomIds.length; i++) {
|
||||||
|
await (update(rooms)..where((r) => r.id.equals(roomIds[i])))
|
||||||
|
.write(RoomsCompanion(sortOrder: Value(i)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Riverpod Stream Provider with Drift
|
||||||
|
**What:** Wrap Drift `.watch()` streams in `@riverpod` annotated functions that return `Stream<T>`. Riverpod auto-wraps in `AsyncValue` for loading/error/data states.
|
||||||
|
**When to use:** All reactive data display (room list, task list, room cards with computed stats).
|
||||||
|
**Example:**
|
||||||
|
```dart
|
||||||
|
// Riverpod 3 code generation pattern (matches existing project style)
|
||||||
|
@riverpod
|
||||||
|
Stream<List<Room>> roomList(Ref ref) {
|
||||||
|
final db = ref.watch(appDatabaseProvider);
|
||||||
|
return db.roomsDao.watchAllRooms();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Family provider for tasks in a specific room
|
||||||
|
@riverpod
|
||||||
|
Stream<List<Task>> tasksInRoom(Ref ref, int roomId) {
|
||||||
|
final db = ref.watch(appDatabaseProvider);
|
||||||
|
return db.tasksDao.watchTasksInRoom(roomId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: AsyncNotifier for Mutations
|
||||||
|
**What:** Class-based `@riverpod` notifier for operations that mutate state (create, update, delete). Follows the existing `ThemeNotifier` pattern.
|
||||||
|
**When to use:** Room CRUD mutations, task CRUD mutations, task completion.
|
||||||
|
**Example:**
|
||||||
|
```dart
|
||||||
|
@riverpod
|
||||||
|
class RoomActions extends _$RoomActions {
|
||||||
|
@override
|
||||||
|
FutureOr<void> build() {}
|
||||||
|
|
||||||
|
Future<int> createRoom(String name, String iconName) async {
|
||||||
|
final db = ref.read(appDatabaseProvider);
|
||||||
|
return db.roomsDao.insertRoom(RoomsCompanion.insert(
|
||||||
|
name: name,
|
||||||
|
iconName: iconName,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteRoom(int roomId) async {
|
||||||
|
final db = ref.read(appDatabaseProvider);
|
||||||
|
await db.roomsDao.deleteRoom(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 5: Pure Scheduling Utility
|
||||||
|
**What:** Stateless utility class/functions for calculating next due dates. No database dependency -- pure date arithmetic.
|
||||||
|
**When to use:** After task completion (TASK-07), during template seeding.
|
||||||
|
**Example:**
|
||||||
|
```dart
|
||||||
|
/// Calculate next due date from the current due date and interval config.
|
||||||
|
/// For calendar-anchored intervals, [anchorDay] is the original day-of-month.
|
||||||
|
DateTime calculateNextDueDate({
|
||||||
|
required DateTime currentDueDate,
|
||||||
|
required IntervalType intervalType,
|
||||||
|
required int intervalDays,
|
||||||
|
int? anchorDay,
|
||||||
|
}) {
|
||||||
|
DateTime next;
|
||||||
|
switch (intervalType) {
|
||||||
|
// Day-count: pure arithmetic
|
||||||
|
case IntervalType.daily:
|
||||||
|
next = currentDueDate.add(const Duration(days: 1));
|
||||||
|
case IntervalType.everyNDays:
|
||||||
|
next = currentDueDate.add(Duration(days: intervalDays));
|
||||||
|
case IntervalType.weekly:
|
||||||
|
next = currentDueDate.add(const Duration(days: 7));
|
||||||
|
case IntervalType.biweekly:
|
||||||
|
next = currentDueDate.add(const Duration(days: 14));
|
||||||
|
// Calendar-anchored: month arithmetic with clamping
|
||||||
|
case IntervalType.monthly:
|
||||||
|
next = _addMonths(currentDueDate, 1, anchorDay);
|
||||||
|
case IntervalType.everyNMonths:
|
||||||
|
next = _addMonths(currentDueDate, intervalDays, anchorDay);
|
||||||
|
case IntervalType.quarterly:
|
||||||
|
next = _addMonths(currentDueDate, 3, anchorDay);
|
||||||
|
case IntervalType.yearly:
|
||||||
|
next = _addMonths(currentDueDate, 12, anchorDay);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add months with day-of-month clamping.
|
||||||
|
/// [anchorDay] remembers the original day for correct clamping.
|
||||||
|
DateTime _addMonths(DateTime date, int months, int? anchorDay) {
|
||||||
|
final targetMonth = date.month + months;
|
||||||
|
final targetYear = date.year + (targetMonth - 1) ~/ 12;
|
||||||
|
final normalizedMonth = ((targetMonth - 1) % 12) + 1;
|
||||||
|
final day = anchorDay ?? date.day;
|
||||||
|
// Last day of target month: day 0 of next month
|
||||||
|
final lastDay = DateTime(targetYear, normalizedMonth + 1, 0).day;
|
||||||
|
final clampedDay = day > lastDay ? lastDay : day;
|
||||||
|
return DateTime(targetYear, normalizedMonth, clampedDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Catch-up: if next due is in the past, keep adding until future/today.
|
||||||
|
DateTime catchUpToPresent({
|
||||||
|
required DateTime nextDue,
|
||||||
|
required DateTime today,
|
||||||
|
required IntervalType intervalType,
|
||||||
|
required int intervalDays,
|
||||||
|
int? anchorDay,
|
||||||
|
}) {
|
||||||
|
while (nextDue.isBefore(today)) {
|
||||||
|
nextDue = calculateNextDueDate(
|
||||||
|
currentDueDate: nextDue,
|
||||||
|
intervalType: intervalType,
|
||||||
|
intervalDays: intervalDays,
|
||||||
|
anchorDay: anchorDay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nextDue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 6: GoRouter Nested Routes
|
||||||
|
**What:** Add child routes under the existing `/rooms` branch for room detail and task forms.
|
||||||
|
**When to use:** Navigation from room grid to room detail, task creation, task editing.
|
||||||
|
**Example:**
|
||||||
|
```dart
|
||||||
|
StatefulShellBranch(
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/rooms',
|
||||||
|
builder: (context, state) => const RoomsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'new',
|
||||||
|
builder: (context, state) => const RoomFormScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: ':roomId',
|
||||||
|
builder: (context, state) {
|
||||||
|
final roomId = int.parse(state.pathParameters['roomId']!);
|
||||||
|
return TaskListScreen(roomId: roomId);
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'edit',
|
||||||
|
builder: (context, state) {
|
||||||
|
final roomId = int.parse(state.pathParameters['roomId']!);
|
||||||
|
return RoomFormScreen(roomId: roomId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'tasks/new',
|
||||||
|
builder: (context, state) {
|
||||||
|
final roomId = int.parse(state.pathParameters['roomId']!);
|
||||||
|
return TaskFormScreen(roomId: roomId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'tasks/:taskId',
|
||||||
|
builder: (context, state) {
|
||||||
|
final taskId = int.parse(state.pathParameters['taskId']!);
|
||||||
|
return TaskFormScreen(taskId: taskId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
- **Putting scheduling logic in the DAO or provider:** Scheduling is pure date math -- keep it in a standalone utility for testability. The DAO should only call the utility, not contain the logic.
|
||||||
|
- **Using `ref.read` in `build()` for reactive data:** Always use `ref.watch` in widget `build()` methods for Drift stream providers. Use `ref.read` only in callbacks (button presses, etc.).
|
||||||
|
- **Storing icon as `IconData` or `int` codePoint:** Store the icon name as a `String` (e.g., `"kitchen"`, `"bathtub"`). Map to `IconData` in the presentation layer. This is human-readable and migration-safe.
|
||||||
|
- **Mixing completion logic with UI:** The "mark done" flow (record completion, calculate next due, update task) should be a single DAO transaction, not scattered across widget callbacks.
|
||||||
|
- **Using `DateTime.now()` directly in scheduling logic:** Accept `today` as a parameter so tests can use fixed dates.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| Reorderable grid | Custom `GestureDetector` + `AnimatedPositioned` | `flutter_reorderable_grid_view` | Drag-and-drop with auto-scroll, animation, and accessibility is deceptively complex |
|
||||||
|
| Database reactive streams | Manual `StreamController` + polling | Drift `.watch()` | Drift tracks table changes automatically and re-emits; manual streams miss updates |
|
||||||
|
| Schema migration | Raw SQL `ALTER TABLE` | Drift `MigrationStrategy` + `Migrator` | Drift validates migrations at compile time with generated helpers |
|
||||||
|
| AsyncValue loading/error | Manual `isLoading` / `hasError` booleans | Riverpod `AsyncValue.when()` | Pattern matching is exhaustive and handles all states correctly |
|
||||||
|
| Relative date formatting | Custom if/else chains | Dedicated `formatRelativeDate()` utility | Centralize German labels ("Heute", "Morgen", "in X Tagen", "Ueberfaellig seit X Tagen") in one place for consistency |
|
||||||
|
|
||||||
|
**Key insight:** The main complexity in this phase is the scheduling logic and the Drift schema -- both are areas where custom, well-tested code is appropriate. The UI components (grid reorder, forms, bottom sheets) should lean on existing Flutter/package capabilities.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Drift intEnum Ordering Instability
|
||||||
|
**What goes wrong:** Adding a new value in the middle of a Dart enum changes the integer indices of all subsequent values, silently corrupting existing database rows.
|
||||||
|
**Why it happens:** `intEnum` stores the enum's `.index` (0, 1, 2...). Inserting a value shifts all following indices.
|
||||||
|
**How to avoid:** Always add new enum values at the END. Never reorder or remove enum values. Consider documenting the index mapping in a comment above the enum.
|
||||||
|
**Warning signs:** Existing tasks suddenly show wrong frequency or effort after a code change.
|
||||||
|
|
||||||
|
### Pitfall 2: Schema Migration from v1 to v2
|
||||||
|
**What goes wrong:** Forgetting to increment `schemaVersion` and add migration logic means the app crashes or silently uses the old schema on existing installs.
|
||||||
|
**Why it happens:** Development uses fresh databases. Real users have v1 databases.
|
||||||
|
**How to avoid:** Increment `schemaVersion` to 2. Use `MigrationStrategy` with `onUpgrade` that calls `m.createTable()` for each new table. Test migration with `NativeDatabase.memory()` by creating a v1 database, then upgrading.
|
||||||
|
**Warning signs:** App works on clean install but crashes on existing install.
|
||||||
|
|
||||||
|
### Pitfall 3: Calendar Month Arithmetic Overflow
|
||||||
|
**What goes wrong:** Adding 1 month to January 31 should give February 28 (or 29), but naive `DateTime(year, month + 1, day)` creates March 3rd because Dart auto-rolls overflow days into the next month.
|
||||||
|
**Why it happens:** Dart `DateTime` constructor auto-normalizes: `DateTime(2026, 2, 31)` becomes `DateTime(2026, 3, 3)`.
|
||||||
|
**How to avoid:** Clamp the day to the last day of the target month using `DateTime(year, month + 1, 0).day`. The anchor-day pattern from CONTEXT.md handles this correctly.
|
||||||
|
**Warning signs:** Monthly tasks due on the 31st drift forward by extra days each month.
|
||||||
|
|
||||||
|
### Pitfall 4: Drift Stream Provider Rebuild Frequency
|
||||||
|
**What goes wrong:** Drift stream queries fire on ANY write to the table, not just rows matching the query's `where` clause. This can cause excessive rebuilds.
|
||||||
|
**Why it happens:** Drift's stream invalidation is table-level, not row-level.
|
||||||
|
**How to avoid:** Keep stream queries focused (filter by room, limit results). Use `ref.select()` in widgets to rebuild only when specific data changes. This is usually not a problem at the scale of a household app but worth knowing.
|
||||||
|
**Warning signs:** UI jank when completing tasks in one room while viewing another.
|
||||||
|
|
||||||
|
### Pitfall 5: Foreign Key Enforcement
|
||||||
|
**What goes wrong:** Drift/SQLite does not enforce foreign keys by default. Deleting a room without deleting its tasks leaves orphaned rows.
|
||||||
|
**Why it happens:** SQLite requires `PRAGMA foreign_keys = ON` to be set per connection.
|
||||||
|
**How to avoid:** Add `beforeOpen` in `MigrationStrategy` that runs `PRAGMA foreign_keys = ON`. Also implement cascade delete explicitly in the DAO transaction as a safety net.
|
||||||
|
**Warning signs:** Orphaned tasks with no room after room deletion.
|
||||||
|
|
||||||
|
### Pitfall 6: Reorderable Grid Key Requirement
|
||||||
|
**What goes wrong:** `flutter_reorderable_grid_view` requires every child to have a unique `Key`. Missing keys cause drag-and-drop to malfunction silently.
|
||||||
|
**Why it happens:** Flutter's reconciliation algorithm needs keys to track moving widgets.
|
||||||
|
**How to avoid:** Use `ValueKey(room.id)` on every room card widget.
|
||||||
|
**Warning signs:** Drag operation doesn't animate correctly, items snap to wrong positions.
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Drift Database Registration with DAOs
|
||||||
|
```dart
|
||||||
|
// Source: drift.simonbinder.eu/dart_api/daos/
|
||||||
|
@DriftDatabase(tables: [Rooms, Tasks, TaskCompletions], daos: [RoomsDao, TasksDao])
|
||||||
|
class AppDatabase extends _$AppDatabase {
|
||||||
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 2;
|
||||||
|
|
||||||
|
@override
|
||||||
|
MigrationStrategy get migration {
|
||||||
|
return MigrationStrategy(
|
||||||
|
onCreate: (Migrator m) async {
|
||||||
|
await m.createAll();
|
||||||
|
},
|
||||||
|
onUpgrade: (Migrator m, int from, int to) async {
|
||||||
|
if (from < 2) {
|
||||||
|
await m.createTable(rooms);
|
||||||
|
await m.createTable(tasks);
|
||||||
|
await m.createTable(taskCompletions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeOpen: (details) async {
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QueryExecutor _openConnection() {
|
||||||
|
return driftDatabase(
|
||||||
|
name: 'household_keeper',
|
||||||
|
native: const DriftNativeOptions(
|
||||||
|
databaseDirectory: getApplicationSupportDirectory,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task Completion Transaction
|
||||||
|
```dart
|
||||||
|
// In TasksDao: mark task done and calculate next due date
|
||||||
|
Future<void> completeTask(int taskId) {
|
||||||
|
return transaction(() async {
|
||||||
|
// 1. Get current task
|
||||||
|
final task = await (select(tasks)..where((t) => t.id.equals(taskId))).getSingle();
|
||||||
|
|
||||||
|
// 2. Record completion
|
||||||
|
await into(taskCompletions).insert(TaskCompletionsCompanion.insert(
|
||||||
|
taskId: taskId,
|
||||||
|
completedAt: DateTime.now(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// 3. Calculate next due date (from original due date, not today)
|
||||||
|
var nextDue = calculateNextDueDate(
|
||||||
|
currentDueDate: task.nextDueDate,
|
||||||
|
intervalType: task.intervalType,
|
||||||
|
intervalDays: task.intervalDays,
|
||||||
|
anchorDay: task.anchorDay,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Catch up if next due is still in the past
|
||||||
|
final today = DateTime.now();
|
||||||
|
final todayDateOnly = DateTime(today.year, today.month, today.day);
|
||||||
|
nextDue = catchUpToPresent(
|
||||||
|
nextDue: nextDue,
|
||||||
|
today: todayDateOnly,
|
||||||
|
intervalType: task.intervalType,
|
||||||
|
intervalDays: task.intervalDays,
|
||||||
|
anchorDay: task.anchorDay,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Update task with new due date
|
||||||
|
await (update(tasks)..where((t) => t.id.equals(taskId)))
|
||||||
|
.write(TasksCompanion(nextDueDate: Value(nextDue)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Room Card with Cleanliness Indicator
|
||||||
|
```dart
|
||||||
|
// Presentation layer pattern
|
||||||
|
class RoomCard extends StatelessWidget {
|
||||||
|
final Room room;
|
||||||
|
final int dueTaskCount;
|
||||||
|
final double cleanlinessRatio; // 0.0 (all overdue) to 1.0 (all on-time)
|
||||||
|
|
||||||
|
const RoomCard({
|
||||||
|
super.key,
|
||||||
|
required this.room,
|
||||||
|
required this.dueTaskCount,
|
||||||
|
required this.cleanlinessRatio,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
// Green -> Yellow -> Red based on cleanliness ratio
|
||||||
|
final barColor = Color.lerp(
|
||||||
|
const Color(0xFFE07A5F), // Warm coral/terracotta (overdue)
|
||||||
|
const Color(0xFF7A9A6D), // Sage green (clean)
|
||||||
|
cleanlinessRatio,
|
||||||
|
)!;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => context.go('/rooms/${room.id}'),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(_mapIcon(room.iconName), size: 36),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(room.name, style: theme.textTheme.titleSmall),
|
||||||
|
if (dueTaskCount > 0)
|
||||||
|
Text('$dueTaskCount faellig',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
)),
|
||||||
|
const Spacer(),
|
||||||
|
// Thin cleanliness bar at bottom
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: cleanlinessRatio,
|
||||||
|
backgroundColor: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
color: barColor,
|
||||||
|
minHeight: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Data Structure (Dart Constants)
|
||||||
|
```dart
|
||||||
|
// Bundled German-language templates as static constants
|
||||||
|
class TaskTemplate {
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
final IntervalType intervalType;
|
||||||
|
final int intervalDays;
|
||||||
|
final EffortLevel effortLevel;
|
||||||
|
|
||||||
|
const TaskTemplate({
|
||||||
|
required this.name,
|
||||||
|
this.description,
|
||||||
|
required this.intervalType,
|
||||||
|
this.intervalDays = 1,
|
||||||
|
required this.effortLevel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room type to template mapping
|
||||||
|
const Map<String, List<TaskTemplate>> roomTemplates = {
|
||||||
|
'kueche': [
|
||||||
|
TaskTemplate(name: 'Abspuelen', intervalType: IntervalType.daily, effortLevel: EffortLevel.low),
|
||||||
|
TaskTemplate(name: 'Kuehlschrank reinigen', intervalType: IntervalType.monthly, effortLevel: EffortLevel.medium),
|
||||||
|
TaskTemplate(name: 'Herd reinigen', intervalType: IntervalType.weekly, effortLevel: EffortLevel.medium),
|
||||||
|
TaskTemplate(name: 'Muell rausbringen', intervalType: IntervalType.everyNDays, intervalDays: 2, effortLevel: EffortLevel.low),
|
||||||
|
// ... more templates
|
||||||
|
],
|
||||||
|
'badezimmer': [
|
||||||
|
TaskTemplate(name: 'Toilette putzen', intervalType: IntervalType.weekly, effortLevel: EffortLevel.medium),
|
||||||
|
TaskTemplate(name: 'Spiegel reinigen', intervalType: IntervalType.weekly, effortLevel: EffortLevel.low),
|
||||||
|
// ... more templates
|
||||||
|
],
|
||||||
|
// ... all 14 room types
|
||||||
|
};
|
||||||
|
|
||||||
|
// Room type detection from name (lightweight matching)
|
||||||
|
String? detectRoomType(String roomName) {
|
||||||
|
final lower = roomName.toLowerCase().trim();
|
||||||
|
for (final type in roomTemplates.keys) {
|
||||||
|
if (lower.contains(type) || _aliases[type]?.any((a) => lower.contains(a)) == true) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _aliases = {
|
||||||
|
'kueche': ['kitchen'],
|
||||||
|
'badezimmer': ['bad', 'wc', 'toilette'],
|
||||||
|
'schlafzimmer': ['schlafraum'],
|
||||||
|
// ... more aliases
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relative Date Formatter (German)
|
||||||
|
```dart
|
||||||
|
/// Format a due date relative to today in German.
|
||||||
|
/// Source: CONTEXT.md user decision on German labels
|
||||||
|
String formatRelativeDate(DateTime dueDate, DateTime today) {
|
||||||
|
final diff = DateTime(dueDate.year, dueDate.month, dueDate.day)
|
||||||
|
.difference(DateTime(today.year, today.month, today.day))
|
||||||
|
.inDays;
|
||||||
|
|
||||||
|
if (diff == 0) return 'Heute';
|
||||||
|
if (diff == 1) return 'Morgen';
|
||||||
|
if (diff > 1) return 'in $diff Tagen';
|
||||||
|
if (diff == -1) return 'Ueberfaellig seit 1 Tag';
|
||||||
|
return 'Ueberfaellig seit ${diff.abs()} Tagen';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Icon Picker Bottom Sheet
|
||||||
|
```dart
|
||||||
|
// Curated household Material Icons (~25 icons)
|
||||||
|
const List<({String name, IconData icon})> curatedRoomIcons = [
|
||||||
|
(name: 'kitchen', icon: Icons.kitchen),
|
||||||
|
(name: 'bathtub', icon: Icons.bathtub),
|
||||||
|
(name: 'bed', icon: Icons.bed),
|
||||||
|
(name: 'living', icon: Icons.living),
|
||||||
|
(name: 'weekend', icon: Icons.weekend),
|
||||||
|
(name: 'door_front', icon: Icons.door_front_door),
|
||||||
|
(name: 'desk', icon: Icons.desk),
|
||||||
|
(name: 'garage', icon: Icons.garage),
|
||||||
|
(name: 'balcony', icon: Icons.balcony),
|
||||||
|
(name: 'local_laundry', icon: Icons.local_laundry_service),
|
||||||
|
(name: 'stairs', icon: Icons.stairs),
|
||||||
|
(name: 'child_care', icon: Icons.child_care),
|
||||||
|
(name: 'single_bed', icon: Icons.single_bed),
|
||||||
|
(name: 'dining', icon: Icons.dining),
|
||||||
|
(name: 'yard', icon: Icons.yard),
|
||||||
|
(name: 'grass', icon: Icons.grass),
|
||||||
|
(name: 'home', icon: Icons.home),
|
||||||
|
(name: 'storage', icon: Icons.inventory_2),
|
||||||
|
(name: 'window', icon: Icons.window),
|
||||||
|
(name: 'cleaning', icon: Icons.cleaning_services),
|
||||||
|
(name: 'iron', icon: Icons.iron),
|
||||||
|
(name: 'microwave', icon: Icons.microwave),
|
||||||
|
(name: 'shower', icon: Icons.shower),
|
||||||
|
(name: 'chair', icon: Icons.chair),
|
||||||
|
(name: 'door_sliding', icon: Icons.door_sliding),
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | When Changed | Impact |
|
||||||
|
|--------------|------------------|--------------|--------|
|
||||||
|
| `StateNotifier` + manual providers | `@riverpod` code gen + `Notifier`/`AsyncNotifier` | Riverpod 3.0 (Sep 2025) | Simpler syntax, auto-dispose by default, unified `Ref` |
|
||||||
|
| Manual `StreamController` for DB reactivity | Drift `.watch()` streams | Drift 2.x | Zero-effort reactive queries, auto-invalidation on writes |
|
||||||
|
| `AutoDisposeStreamProvider` | `@riverpod` returning `Stream<T>` | Riverpod 3.0 | Code generator infers provider type from return type |
|
||||||
|
| Custom drag-and-drop with `Draggable`+`DragTarget` | `flutter_reorderable_grid_view` 5.x | 2025 v5 rewrite | Performance overhaul, smooth animations, auto-scrolling |
|
||||||
|
|
||||||
|
**Deprecated/outdated:**
|
||||||
|
- `StateNotifier`/`StateNotifierProvider`: moved to `legacy.dart` import in Riverpod 3.0. Use `Notifier` instead.
|
||||||
|
- `StateProvider`: also legacy. Use functional `@riverpod` providers.
|
||||||
|
- Old Riverpod generated ref types (e.g., `AppDatabaseRef`): Riverpod 3 uses plain `Ref`.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Exact number of templates per room type**
|
||||||
|
- What we know: 14 room types need templates with German names, frequencies, and effort levels
|
||||||
|
- What's unclear: How many templates per type (3-8 seems reasonable for usability)
|
||||||
|
- Recommendation: Start with 4-6 templates per room type. Easy to expand later since they're Dart constants.
|
||||||
|
|
||||||
|
2. **Room form as full screen vs bottom sheet**
|
||||||
|
- What we know: Discretion area. Room creation needs name input + icon picker.
|
||||||
|
- What's unclear: Whether the flow (name -> icon -> optional templates) fits well in a bottom sheet or needs full screen
|
||||||
|
- Recommendation: Full-screen form for room creation/edit. It needs a text field, icon picker grid, and potentially template selection. Bottom sheets with keyboard input have known usability issues (keyboard covering content). The template picker that appears after creation can be a separate bottom sheet.
|
||||||
|
|
||||||
|
3. **Task form layout**
|
||||||
|
- What we know: Discretion area. Tasks need name, optional description, frequency, effort.
|
||||||
|
- What's unclear: Best field ordering and grouping
|
||||||
|
- Recommendation: Full-screen form. Fields ordered: name (required, autofocus), frequency (required, segmented button + custom picker), effort (required, 3-option segmented button), description (optional, multiline text). Group required fields at top.
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Framework | flutter_test (built-in) |
|
||||||
|
| Config file | none -- standard Flutter test setup |
|
||||||
|
| Quick run command | `flutter test test/features/rooms/ test/features/tasks/ -x` |
|
||||||
|
| Full suite command | `flutter test` |
|
||||||
|
|
||||||
|
### Phase Requirements to Test Map
|
||||||
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||||
|
|--------|----------|-----------|-------------------|-------------|
|
||||||
|
| ROOM-01 | Insert room with name + icon via DAO | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 |
|
||||||
|
| ROOM-02 | Update room name/icon via DAO | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 |
|
||||||
|
| ROOM-03 | Delete room cascades tasks + completions | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 |
|
||||||
|
| ROOM-04 | Reorder rooms updates sortOrder | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 |
|
||||||
|
| ROOM-05 | Watch rooms stream emits with task counts | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-01 | Insert task with all fields via DAO | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-02 | Update task fields via DAO | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-03 | Delete task with confirmation | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-04 | All preset intervals produce correct next due dates | unit | `flutter test test/features/tasks/domain/scheduling_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-05 | Effort level stored and retrieved correctly | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-06 | Tasks sorted by due date in query | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-07 | Complete task records completion + updates next due | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 |
|
||||||
|
| TASK-08 | Overdue detection based on date comparison | unit | `flutter test test/features/tasks/domain/scheduling_test.dart -x` | Wave 0 |
|
||||||
|
| TMPL-01 | Template data contains valid entries for each room type | unit | `flutter test test/features/templates/task_templates_test.dart -x` | Wave 0 |
|
||||||
|
| TMPL-02 | All 14 room types have templates | unit | `flutter test test/features/templates/task_templates_test.dart -x` | Wave 0 |
|
||||||
|
|
||||||
|
### Sampling Rate
|
||||||
|
- **Per task commit:** `flutter test test/features/rooms/ test/features/tasks/ test/features/templates/`
|
||||||
|
- **Per wave merge:** `flutter test`
|
||||||
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
||||||
|
|
||||||
|
### Wave 0 Gaps
|
||||||
|
- [ ] `test/features/rooms/data/rooms_dao_test.dart` -- covers ROOM-01 through ROOM-05
|
||||||
|
- [ ] `test/features/tasks/data/tasks_dao_test.dart` -- covers TASK-01 through TASK-03, TASK-05 through TASK-07
|
||||||
|
- [ ] `test/features/tasks/domain/scheduling_test.dart` -- covers TASK-04, TASK-07 next due logic, TASK-08 overdue detection, calendar-anchored clamping, catch-up logic
|
||||||
|
- [ ] `test/features/templates/task_templates_test.dart` -- covers TMPL-01, TMPL-02 (all 14 room types present, valid data)
|
||||||
|
- [ ] Framework already installed; no additional test dependencies needed.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- [Drift official docs - Tables](https://drift.simonbinder.eu/dart_api/tables/) - Table definition syntax, column types, enums, foreign keys, autoIncrement
|
||||||
|
- [Drift official docs - DAOs](https://drift.simonbinder.eu/dart_api/daos/) - DAO definition, annotation, CRUD grouping
|
||||||
|
- [Drift official docs - Writes](https://drift.simonbinder.eu/dart_api/writes/) - Insert with companions, insertReturning, update with where, delete, batch
|
||||||
|
- [Drift official docs - Select](https://drift.simonbinder.eu/dart_api/select/) - Select with where, watch/stream, orderBy, joins
|
||||||
|
- [Drift official docs - Streams](https://drift.simonbinder.eu/dart_api/streams/) - Stream query mechanism, update triggers, performance notes
|
||||||
|
- [Drift official docs - Migrations](https://drift.simonbinder.eu/migrations/) - Schema version, MigrationStrategy, onUpgrade, createTable, PRAGMA foreign_keys
|
||||||
|
- [Drift official docs - Type converters](https://drift.simonbinder.eu/type_converters/) - intEnum, textEnum, cautionary notes on enum ordering
|
||||||
|
- [Flutter ReorderableListView API](https://api.flutter.dev/flutter/material/ReorderableListView-class.html) - Built-in reorderable list reference
|
||||||
|
- Existing project codebase (`database.dart`, `database_provider.dart`, `theme_provider.dart`, `settings_screen.dart`, `rooms_screen.dart`, `router.dart`) - Established patterns for Riverpod 3, Drift, GoRouter
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- [Riverpod 3.0 What's New](https://riverpod.dev/docs/whats_new) - Riverpod 3 changes, StreamProvider with code gen, Ref unification
|
||||||
|
- [flutter_reorderable_grid_view pub.dev](https://pub.dev/packages/flutter_reorderable_grid_view) - v5.6.0, ReorderableBuilder API, ScrollController handling
|
||||||
|
- [Code with Andrea - AsyncNotifier guide](https://codewithandrea.com/articles/flutter-riverpod-async-notifier/) - AsyncNotifier CRUD patterns
|
||||||
|
- [Code with Andrea - Riverpod Generator guide](https://codewithandrea.com/articles/flutter-riverpod-generator/) - @riverpod Stream return type generates StreamProvider
|
||||||
|
- [Dart DateTime API docs](https://api.dart.dev/dart-core/DateTime-class.html) - DateTime constructor auto-normalization behavior
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence)
|
||||||
|
- [DeepWiki - sample_drift_app state management](https://deepwiki.com/h-enoki/sample_drift_app/5.1-state-management-with-riverpod) - Drift + Riverpod integration patterns (community source, single example)
|
||||||
|
- [GitHub riverpod issue #3832](https://github.com/rrousselGit/riverpod/issues/3832) - StreamProvider vs StreamBuilder behavior differences (edge case)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- Standard stack: HIGH - All libraries already in project except flutter_reorderable_grid_view (well-established package)
|
||||||
|
- Architecture: HIGH - Patterns directly extend Phase 1 established conventions, verified against existing code
|
||||||
|
- Pitfalls: HIGH - Drift enum ordering, migration, and PRAGMA foreign_keys are well-documented official concerns
|
||||||
|
- Scheduling logic: HIGH - Rules are fully specified in CONTEXT.md, Dart DateTime behavior verified against API docs
|
||||||
|
- Templates: MEDIUM - Template content (exact tasks per room type) needs to be authored, but structure and approach are straightforward
|
||||||
|
|
||||||
|
**Research date:** 2026-03-15
|
||||||
|
**Valid until:** 2026-04-15 (stable stack, no fast-moving dependencies)
|
||||||
Reference in New Issue
Block a user