# 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)
