# Phase 3: Daily Plan and Cleanliness - Research

**Researched:** 2026-03-16
**Domain:** Flutter daily plan screen with cross-room Drift queries, animated task completion, sectioned list UI, progress indicators
**Confidence:** HIGH

## Summary

Phase 3 transforms the placeholder Home tab into the app's primary "daily plan" screen -- the first thing users see when opening HouseHoldKeaper. The screen needs three key capabilities: (1) a cross-room Drift query that watches all tasks and categorizes them by due date (overdue, today, tomorrow), (2) animated task removal on checkbox completion with the existing `TasksDao.completeTask()` logic, and (3) a progress indicator card showing "X von Y erledigt" that updates in real-time.

The existing codebase provides strong foundations: `TasksDao.completeTask()` already handles completion + scheduling in a single transaction, `formatRelativeDate()` produces German date labels, `TaskRow` provides the baseline task row widget (needs adaptation), and the Riverpod stream provider pattern is well-established. The main new work is: (a) a new DAO method that joins tasks with rooms for cross-room queries, (b) a `DailyPlanTaskRow` widget variant with room name tag and no row-tap navigation, (c) `AnimatedList` for slide-out completion animation, (d) `ExpansionTile` for the collapsible "Demnachst" section, and (e) new localization strings.

CLEAN-01 (cleanliness indicator on room cards) is already fully implemented in Phase 2 via `RoomWithStats.cleanlinessRatio` and the `LinearProgressIndicator` bar at the bottom of `RoomCard`. This requirement needs only verification, not new implementation.

**Primary recommendation:** Add a single new DAO method `watchAllTasksWithRoomName()` that joins tasks with rooms, expose it through a manual `StreamProvider` (same pattern as `tasksInRoomProvider` due to drift type issues), derive overdue/today/tomorrow categorization in the provider layer, and build the daily plan screen as a `CustomScrollView` with `SliverAnimatedList` for animated completion.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **Daily plan screen structure**: Single scroll list with three section headers: Uberfaellig, Heute, Demnachst. Flat task list within each section -- tasks are not grouped under room sub-headers. Each task row shows room name as an inline tappable tag that navigates to that room's task list. Progress indicator at the very top as a prominent card/banner ("5 von 12 erledigt") -- first thing the user sees. Overdue section only appears when there are overdue tasks. Demnachst section is collapsed by default -- shows header with count (e.g. "Demnachst (4)"), expands on tap. PLAN-01 "grouped by room" is satisfied by room name shown on each task -- not visual sub-grouping.
- **Task completion on daily plan**: Checkbox only -- no swipe-to-complete gesture. Consistent with Phase 2 room task list. Completed tasks animate out of the list (slide away). Progress counter updates immediately. No navigation from tapping task rows -- the daily plan is a focused "get things done" screen. Only the checkbox and the room name tag are interactive. Completion behavior identical to Phase 2: immediate, no undo, records timestamp, auto-calculates next due date.
- **Upcoming tasks scope**: Tomorrow only -- Demnachst shows tasks due the next calendar day. Read-only preview -- no checkboxes, tasks cannot be completed ahead of schedule from the daily plan. Collapsed by default to keep focus on today's actionable tasks.

### Claude's Discretion
- "All clear" empty state design (follow Phase 1's playful, emoji-friendly German tone with the established visual pattern: Material icon + message + optional action)
- Task row adaptation for daily plan context (may differ from TaskRow in room view since no row-tap navigation and room name tag is added)
- Exact animation for task completion (slide direction, duration, easing)
- Progress card/banner visual design (linear progress bar, circular, or text-only)
- Section header styling and the collapsed/expanded toggle for Demnachst
- How overdue tasks are sorted within the flat list (most overdue first, or by room, or alphabetical)

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| PLAN-01 | User sees all tasks due today grouped by room on the daily plan screen | New DAO join query `watchAllTasksWithRoomName()`, flat list with room name tag on each row satisfies "grouped by room" per CONTEXT.md |
| PLAN-02 | Overdue tasks appear in a separate highlighted section at the top | Provider-layer date categorization splits tasks into overdue/today/tomorrow sections; overdue section conditionally rendered |
| PLAN-03 | User can preview upcoming tasks (tomorrow) | Demnachst section with `ExpansionTile` collapsed by default, read-only rows (no checkbox) |
| PLAN-04 | User can checkbox to mark tasks done from daily plan | Reuse existing `taskActionsProvider.completeTask()`, `AnimatedList.removeItem()` for slide-out animation |
| PLAN-05 | Progress indicator showing completed vs total tasks today | Computed from stream data: `completedToday` count from `TaskCompletions` + `totalToday` from due tasks. Progress card at top of screen |
| PLAN-06 | "All clear" empty state when no tasks are due | Established empty state pattern (Material icon + message + optional action) in German |
| CLEAN-01 | Each room card displays cleanliness indicator | Already implemented in Phase 2 via `RoomWithStats.cleanlinessRatio` and `RoomCard` -- verification only |
</phase_requirements>

## Standard Stack

### Core (already in project)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| drift | 2.31.0 | Type-safe SQLite ORM with join queries | Already established; provides `leftOuterJoin`, `.watch()` streams for cross-room task queries |
| flutter_riverpod | 3.3.1 | State management | Already established; `StreamProvider` pattern for reactive daily plan data |
| riverpod_annotation | 4.0.2 | Provider code generation | `@riverpod` for generated providers |
| go_router | 17.1.0 | Declarative routing | Room name tag navigation uses `context.go('/rooms/$roomId')` |
| flutter (SDK) | 3.41.1 | Framework | Provides `AnimatedList`, `ExpansionTile`, `LinearProgressIndicator` |

### New Dependencies
None. Phase 3 uses only Flutter built-in widgets and existing project dependencies.

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| `AnimatedList` with manual state sync | Simple `ListView` with `AnimatedSwitcher` | `AnimatedList` provides proper slide-out with `removeItem()`. `AnimatedSwitcher` only fades, no size collapse. Use `AnimatedList`. |
| `ExpansionTile` for Demnachst | Custom `AnimatedContainer` | `ExpansionTile` is Material 3 native, handles expand/collapse animation, arrow icon, and state automatically. No reason to hand-roll. |
| `LinearProgressIndicator` in card | `CircularProgressIndicator` or custom radial | Linear is simpler, more glanceable for "X von Y" context, and matches the progress bar pattern already on room cards. Use linear. |
| Drift join query | In-memory join via multiple stream providers | Drift join runs in SQLite, more efficient for large task counts, and produces a single reactive stream. In-memory join requires watching two streams and combining, more complex. |

## Architecture Patterns

### Recommended Project Structure
```
lib/
  features/
    home/
      data/
        daily_plan_dao.dart       # New DAO for cross-room task queries
        daily_plan_dao.g.dart
      domain/
        daily_plan_models.dart    # TaskWithRoom data class, DailyPlanState
      presentation/
        home_screen.dart          # Replace current placeholder (COMPLETE REWRITE)
        daily_plan_providers.dart  # Riverpod providers for daily plan data
        daily_plan_providers.g.dart
        daily_plan_task_row.dart  # Task row variant for daily plan context
        progress_card.dart        # "X von Y erledigt" progress banner
    tasks/
      data/
        tasks_dao.dart            # Unchanged -- reuse completeTask()
      presentation/
        task_providers.dart       # Unchanged -- reuse taskActionsProvider
  l10n/
    app_de.arb                    # Add ~10 new localization keys
```

### Pattern 1: Drift Join Query for Tasks with Room Name
**What:** A DAO method that joins the tasks table with rooms to produce task objects paired with their room name, watched as a reactive stream.
**When to use:** Daily plan needs all tasks across all rooms with room name for display.
**Example:**
```dart
// Source: drift.simonbinder.eu/dart_api/select/ (join documentation)

/// A task paired with its room name for daily plan display.
class TaskWithRoom {
  final Task task;
  final String roomName;
  final int roomId;

  const TaskWithRoom({
    required this.task,
    required this.roomName,
    required this.roomId,
  });
}

@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
class DailyPlanDao extends DatabaseAccessor<AppDatabase>
    with _$DailyPlanDaoMixin {
  DailyPlanDao(super.attachedDatabase);

  /// Watch all tasks joined with room name, sorted by nextDueDate ascending.
  Stream<List<TaskWithRoom>> watchAllTasksWithRoomName() {
    final query = select(tasks).join([
      innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
    ]);
    query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);

    return query.watch().map((rows) {
      return rows.map((row) {
        final task = row.readTable(tasks);
        final room = row.readTable(rooms);
        return TaskWithRoom(
          task: task,
          roomName: room.name,
          roomId: room.id,
        );
      }).toList();
    });
  }

  /// Count completions recorded today (for progress tracking).
  /// Counts tasks completed today regardless of their current due date.
  Stream<int> watchCompletionsToday({DateTime? now}) {
    final today = now ?? DateTime.now();
    final startOfDay = DateTime(today.year, today.month, today.day);
    final endOfDay = startOfDay.add(const Duration(days: 1));

    final query = selectOnly(taskCompletions)
      ..addColumns([taskCompletions.id.count()])
      ..where(taskCompletions.completedAt.isBiggerOrEqualValue(startOfDay) &
          taskCompletions.completedAt.isSmallerThanValue(endOfDay));

    return query.watchSingle().map((row) {
      return row.read(taskCompletions.id.count()) ?? 0;
    });
  }
}
```

### Pattern 2: Provider-Layer Date Categorization
**What:** A Riverpod provider that watches the raw task stream and categorizes tasks into overdue, today, and tomorrow sections. Also computes progress stats.
**When to use:** Transforming flat task data into the three-section daily plan structure.
**Example:**
```dart
/// Daily plan data categorized into sections.
class DailyPlanState {
  final List<TaskWithRoom> overdueTasks;
  final List<TaskWithRoom> todayTasks;
  final List<TaskWithRoom> tomorrowTasks;
  final int completedTodayCount;
  final int totalTodayCount; // overdue + today (actionable tasks)

  const DailyPlanState({
    required this.overdueTasks,
    required this.todayTasks,
    required this.tomorrowTasks,
    required this.completedTodayCount,
    required this.totalTodayCount,
  });
}

// Manual StreamProvider (same pattern as tasksInRoomProvider)
// due to drift Task type issue with riverpod_generator
final dailyPlanProvider =
    StreamProvider.autoDispose<DailyPlanState>((ref) {
  final db = ref.watch(appDatabaseProvider);
  final taskStream = db.dailyPlanDao.watchAllTasksWithRoomName();
  final completionStream = db.dailyPlanDao.watchCompletionsToday();

  // Combine both streams using Dart's asyncMap pattern
  return taskStream.asyncMap((allTasks) async {
    // Get today's completion count (latest value)
    final completedToday = await db.dailyPlanDao
        .watchCompletionsToday().first;

    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final tomorrow = today.add(const Duration(days: 1));
    final dayAfterTomorrow = tomorrow.add(const Duration(days: 1));

    final overdue = <TaskWithRoom>[];
    final todayList = <TaskWithRoom>[];
    final tomorrowList = <TaskWithRoom>[];

    for (final tw in allTasks) {
      final dueDate = DateTime(
        tw.task.nextDueDate.year,
        tw.task.nextDueDate.month,
        tw.task.nextDueDate.day,
      );
      if (dueDate.isBefore(today)) {
        overdue.add(tw);
      } else if (dueDate.isBefore(tomorrow)) {
        todayList.add(tw);
      } else if (dueDate.isBefore(dayAfterTomorrow)) {
        tomorrowList.add(tw);
      }
    }

    return DailyPlanState(
      overdueTasks: overdue, // already sorted by dueDate from query
      todayTasks: todayList,
      tomorrowTasks: tomorrowList,
      completedTodayCount: completedToday,
      totalTodayCount: overdue.length + todayList.length + completedToday,
    );
  });
});
```

### Pattern 3: AnimatedList for Task Completion Slide-Out
**What:** Use `AnimatedList` with `GlobalKey<AnimatedListState>` to animate task removal when checkbox is tapped. The removed item slides horizontally and collapses vertically.
**When to use:** Overdue and Today sections where tasks have checkboxes.
**Example:**
```dart
// Slide-out animation for completed tasks
void _onTaskCompleted(int index, TaskWithRoom taskWithRoom) {
  // 1. Trigger database completion (fire-and-forget)
  ref.read(taskActionsProvider.notifier).completeTask(taskWithRoom.task.id);

  // 2. Animate the item out of the list
  _listKey.currentState?.removeItem(
    index,
    (context, animation) {
      // Combine slide + size collapse for smooth exit
      return SizeTransition(
        sizeFactor: animation,
        child: SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(1.0, 0.0), // slide right
            end: Offset.zero,
          ).animate(CurvedAnimation(
            parent: animation,
            curve: Curves.easeInOut,
          )),
          child: DailyPlanTaskRow(
            taskWithRoom: taskWithRoom,
            showCheckbox: true,
            onCompleted: () {},
          ),
        ),
      );
    },
    duration: const Duration(milliseconds: 300),
  );
}
```

### Pattern 4: ExpansionTile for Collapsible Demnachst Section
**What:** Use Flutter's built-in `ExpansionTile` for the "Demnachst (N)" collapsible section. Starts collapsed per user decision.
**When to use:** Tomorrow tasks section that is read-only and collapsed by default.
**Example:**
```dart
ExpansionTile(
  initiallyExpanded: false,
  title: Text(
    '${l10n.dailyPlanUpcoming} (${tomorrowTasks.length})',
    style: theme.textTheme.titleMedium,
  ),
  children: tomorrowTasks.map((tw) => DailyPlanTaskRow(
    taskWithRoom: tw,
    showCheckbox: false, // read-only, no completion from daily plan
    onRoomTap: () => context.go('/rooms/${tw.roomId}'),
  )).toList(),
);
```

### Pattern 5: Progress Card with LinearProgressIndicator
**What:** A card/banner at the top of the daily plan showing "X von Y erledigt" with a linear progress bar beneath.
**When to use:** First widget in the daily plan scroll, shows today's completion progress.
**Example:**
```dart
class ProgressCard extends StatelessWidget {
  final int completed;
  final int total;

  const ProgressCard({
    super.key,
    required this.completed,
    required this.total,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppLocalizations.of(context);
    final progress = total > 0 ? completed / total : 0.0;

    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.dailyPlanProgress(completed, total),
              style: theme.textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: LinearProgressIndicator(
                value: progress,
                minHeight: 8,
                backgroundColor: theme.colorScheme.surfaceContainerHighest,
                color: theme.colorScheme.primary,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

### Anti-Patterns to Avoid
- **Using `AnimatedList` for ALL sections (including Demnachst):** The tomorrow section is read-only -- no items are added or removed dynamically. A plain `Column` inside `ExpansionTile` is simpler and avoids unnecessary `GlobalKey` management.
- **Rebuilding `AnimatedList` on every stream emission:** `AnimatedList` requires imperative `insertItem`/`removeItem` calls. Rebuilding the widget discards animation state. The list must synchronize imperatively with stream data, OR use a simpler approach where completion removes from a local list and the stream handles the rest after animation completes.
- **Using a single monolithic `AnimatedList` for all three sections:** Each section has different behavior (overdue: checkbox, today: checkbox, tomorrow: no checkbox, collapsible). Use separate widgets per section.
- **Computing progress from stream-only data:** After completing a task, it moves to a future due date and disappears from "today". The completion count must come from `TaskCompletions` table (tasks completed today), not from the absence of tasks in the stream.
- **Navigating on task row tap:** Per user decision, daily plan task rows have NO row-tap navigation. Only the checkbox and room name tag are interactive. Do NOT reuse `TaskRow` directly -- it has `onTap` navigating to edit form.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Expand/collapse section | Custom `AnimatedContainer` + boolean state | `ExpansionTile` | Material 3 native, handles animation, arrow icon, state persistence automatically |
| Cross-room task query | Multiple stream providers + in-memory merge | Drift `join` query with `.watch()` | Single SQLite query is more efficient and produces one reactive stream |
| Progress bar | Custom `CustomPainter` circular indicator | `LinearProgressIndicator` with `value` | Built-in Material 3 widget, themed automatically, supports determinate mode |
| Slide-out animation | Manual `AnimationController` per row | `AnimatedList.removeItem()` with `SizeTransition` + `SlideTransition` | Framework handles list index bookkeeping and animation lifecycle |
| Date categorization | Separate DB queries per category | Single query + in-memory partitioning | One Drift stream for all tasks, partitioned in Dart. Fewer database watchers, simpler invalidation |

**Key insight:** Phase 3 is primarily a presentation-layer phase. The data layer changes are minimal (one new DAO method + registering the DAO). Most complexity is in the UI: synchronizing `AnimatedList` state with reactive stream data, and building a polished sectioned scroll view with proper animation.

## Common Pitfalls

### Pitfall 1: AnimatedList State Desynchronization
**What goes wrong:** `AnimatedList` requires imperative `insertItem()`/`removeItem()` calls to stay in sync with the data. If the widget rebuilds from a new stream emission while an animation is in progress, the list state and data diverge, causing index-out-of-bounds or duplicate item errors.
**Why it happens:** `AnimatedList` maintains its own internal item count. Stream emissions update the data list independently. If a completion triggers both an `AnimatedList.removeItem()` call AND a stream re-emission (because Drift sees the task's `nextDueDate` changed), the item gets "removed" twice.
**How to avoid:** Use one of two approaches: (A) Optimistic local list: maintain a local `List<TaskWithRoom>` that is initialized from the stream but modified locally on completion. Only re-sync from the stream when a new emission arrives that differs from the local state. (B) Simpler approach: skip `AnimatedList` entirely and use `AnimatedSwitcher` per item with `SizeTransition`, or use `AnimatedList` only for the removal animation then let the stream rebuild the list normally after animation completes. Approach (B) is simpler -- animate out, then after the animation duration, the stream naturally excludes the completed task.
**Warning signs:** "RangeError: index out of range" or items flickering during completion animation.

### Pitfall 2: Progress Count Accuracy After Completion
**What goes wrong:** Counting "completed today" by subtracting current due tasks from an initial count loses accuracy. A task completed today moves its `nextDueDate` to a future date, so it disappears from both overdue and today. Without tracking completions separately, the progress denominator shrinks and the bar appears to jump backward.
**Why it happens:** The total tasks due today changes as tasks are completed (they move to future dates). If you compute `total = overdue.length + today.length`, the total decreases with each completion, making progress misleading (e.g., 0/5 -> complete one -> 0/4 instead of 1/5).
**How to avoid:** Track `completedTodayCount` from the `TaskCompletions` table (count of completions where `completedAt` is today). Compute `totalToday = remainingOverdue + remainingToday + completedTodayCount`. This way, as tasks are completed, `completedTodayCount` increases and `remaining` decreases, keeping the total stable.
**Warning signs:** Progress bar shows "0 von 3 erledigt", complete one task, shows "0 von 2 erledigt" instead of "1 von 3 erledigt".

### Pitfall 3: Drift Stream Over-Emission on Cross-Table Join
**What goes wrong:** A join query watching both `tasks` and `rooms` tables re-fires whenever ANY write happens to either table -- not just relevant rows. Room reordering, for example, triggers a daily plan re-query even though no task data changed.
**Why it happens:** Drift's stream invalidation is table-level, not row-level.
**How to avoid:** This is generally acceptable at household-app scale (dozens of tasks, not thousands). If needed, use `ref.select()` in the widget to avoid rebuilding when the data hasn't meaningfully changed. Alternatively, `distinctUntilChanged` on the stream (using `ListEquality` from `collection` package) prevents redundant widget rebuilds.
**Warning signs:** Daily plan screen rebuilds when user reorders rooms on the Rooms tab.

### Pitfall 4: Empty State vs Loading State Confusion
**What goes wrong:** Showing the "all clear" empty state while data is still loading gives users a false impression that nothing is due.
**Why it happens:** `AsyncValue.when()` with `data: []` is indistinguishable from "no tasks at all" vs "tasks haven't loaded yet" if not handled carefully.
**How to avoid:** Always handle `loading`, `error`, and `data` states in `asyncValue.when()`. Show a subtle progress indicator during loading. Only show the "all clear" empty state when `data` is loaded AND both overdue and today lists are empty.
**Warning signs:** App briefly flashes "Alles erledigt!" on startup before tasks load.

### Pitfall 5: Room Name Tag Navigation Conflict with Tab Shell
**What goes wrong:** Tapping the room name tag on a daily plan task should navigate to that room's task list, but `context.go('/rooms/$roomId')` is on a different tab branch. The navigation switches tabs, which may lose scroll position on the Home tab.
**Why it happens:** GoRouter's `StatefulShellRoute.indexedStack` preserves tab state, but `context.go('/rooms/$roomId')` navigates within the Rooms branch, switching the active tab.
**How to avoid:** This is actually the desired behavior -- the user explicitly tapped the room tag to navigate there. The Home tab state is preserved by `indexedStack` and will be restored when the user taps back to the Home tab. No special handling needed beyond using `context.go('/rooms/$roomId')`.
**Warning signs:** None expected -- this is standard GoRouter tab behavior.

## Code Examples

### Complete DailyPlanDao with Join Query
```dart
// Source: drift.simonbinder.eu/dart_api/select/ (joins documentation)
import 'package:drift/drift.dart';
import '../../../core/database/database.dart';

part 'daily_plan_dao.g.dart';

/// A task paired with its room for daily plan display.
class TaskWithRoom {
  final Task task;
  final String roomName;
  final int roomId;

  const TaskWithRoom({
    required this.task,
    required this.roomName,
    required this.roomId,
  });
}

@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
class DailyPlanDao extends DatabaseAccessor<AppDatabase>
    with _$DailyPlanDaoMixin {
  DailyPlanDao(super.attachedDatabase);

  /// Watch all tasks joined with room name, sorted by nextDueDate ascending.
  /// Includes ALL tasks (overdue, today, future) -- filtering is done in the
  /// provider layer to avoid multiple queries.
  Stream<List<TaskWithRoom>> watchAllTasksWithRoomName() {
    final query = select(tasks).join([
      innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
    ]);
    query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);

    return query.watch().map((rows) {
      return rows.map((row) {
        final task = row.readTable(tasks);
        final room = row.readTable(rooms);
        return TaskWithRoom(
          task: task,
          roomName: room.name,
          roomId: room.id,
        );
      }).toList();
    });
  }

  /// Count task completions recorded today.
  Stream<int> watchCompletionsToday({DateTime? today}) {
    final now = today ?? DateTime.now();
    final startOfDay = DateTime(now.year, now.month, now.day);
    final endOfDay = startOfDay.add(const Duration(days: 1));

    return customSelect(
      'SELECT COUNT(*) AS c FROM task_completions '
      'WHERE completed_at >= ? AND completed_at < ?',
      variables: [Variable(startOfDay), Variable(endOfDay)],
      readsFrom: {taskCompletions},
    ).watchSingle().map((row) => row.read<int>('c'));
  }
}
```

### Daily Plan Task Row (Adapted from TaskRow)
```dart
// Source: existing TaskRow pattern adapted per CONTEXT.md decisions
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

/// Warm coral/terracotta color for overdue styling (reused from TaskRow).
const _overdueColor = Color(0xFFE07A5F);

class DailyPlanTaskRow extends ConsumerWidget {
  const DailyPlanTaskRow({
    super.key,
    required this.taskWithRoom,
    required this.showCheckbox,
    this.onCompleted,
  });

  final TaskWithRoom taskWithRoom;
  final bool showCheckbox; // false for tomorrow (read-only)
  final VoidCallback? onCompleted;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = Theme.of(context);
    final task = taskWithRoom.task;
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final dueDate = DateTime(
      task.nextDueDate.year,
      task.nextDueDate.month,
      task.nextDueDate.day,
    );
    final isOverdue = dueDate.isBefore(today);
    final relativeDateText = formatRelativeDate(task.nextDueDate, now);

    return ListTile(
      leading: showCheckbox
          ? Checkbox(
              value: false,
              onChanged: (_) => onCompleted?.call(),
            )
          : null,
      title: Text(
        task.name,
        style: theme.textTheme.titleMedium,
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
      ),
      subtitle: Row(
        children: [
          // Room name tag (tappable)
          GestureDetector(
            onTap: () => context.go('/rooms/${taskWithRoom.roomId}'),
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
              decoration: BoxDecoration(
                color: theme.colorScheme.secondaryContainer,
                borderRadius: BorderRadius.circular(4),
              ),
              child: Text(
                taskWithRoom.roomName,
                style: theme.textTheme.labelSmall?.copyWith(
                  color: theme.colorScheme.onSecondaryContainer,
                ),
              ),
            ),
          ),
          const SizedBox(width: 8),
          Text(
            relativeDateText,
            style: theme.textTheme.bodySmall?.copyWith(
              color: isOverdue
                  ? _overdueColor
                  : theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
      // NO onTap -- daily plan is a focused "get things done" screen
    );
  }
}
```

### "All Clear" Empty State
```dart
// Source: established empty state pattern from HomeScreen and TaskListScreen
Widget _buildAllClearState(BuildContext context) {
  final theme = Theme.of(context);
  final l10n = AppLocalizations.of(context);

  return Center(
    child: Padding(
      padding: const EdgeInsets.symmetric(horizontal: 32),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            Icons.celebration_outlined,
            size: 80,
            color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
          ),
          const SizedBox(height: 24),
          Text(
            l10n.dailyPlanAllClearTitle,
            style: theme.textTheme.headlineSmall,
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 8),
          Text(
            l10n.dailyPlanAllClearMessage,
            style: theme.textTheme.bodyLarge?.copyWith(
              color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
            ),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    ),
  );
}
```

### New Localization Keys (app_de.arb additions)
```json
{
  "dailyPlanProgress": "{completed} von {total} erledigt",
  "@dailyPlanProgress": {
    "placeholders": {
      "completed": { "type": "int" },
      "total": { "type": "int" }
    }
  },
  "dailyPlanSectionOverdue": "Ueberfaellig",
  "dailyPlanSectionToday": "Heute",
  "dailyPlanSectionUpcoming": "Demnachst",
  "dailyPlanUpcomingCount": "Demnachst ({count})",
  "@dailyPlanUpcomingCount": {
    "placeholders": {
      "count": { "type": "int" }
    }
  },
  "dailyPlanAllClearTitle": "Alles erledigt!",
  "dailyPlanAllClearMessage": "Keine Aufgaben fuer heute. Geniesse den Moment!"
}
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Multiple separate DB queries for overdue/today/tomorrow | Single join query with in-memory partitioning | Best practice with Drift 2.x | Fewer database watchers, one stream invalidation path |
| `rxdart` `CombineLatest` for merging streams | `ref.watch()` on multiple providers in Riverpod 3 | Riverpod 3.0 (Sep 2025) | `ref.watch(provider.stream)` removed; use computed providers instead |
| `ExpansionTileController` | `ExpansibleController` (Flutter 3.32+) | Flutter 3.32 | `ExpansionTileController` deprecated in favor of `ExpansibleController` |
| `LinearProgressIndicator` 2023 design | `year2023: false` for 2024 design spec | Flutter 3.41+ | New design with rounded corners, gap between tracks. Still defaulting to 2023 design unless opted in. |

**Deprecated/outdated:**
- `ExpansionTileController`: Deprecated after Flutter 3.31. Use `ExpansibleController` for programmatic expand/collapse. However, `ExpansionTile` still works with `initiallyExpanded` without needing a controller.
- `ref.watch(provider.stream)`: Removed in Riverpod 3. Cannot access underlying stream directly. Use `ref.watch` on the provider value instead.

## Open Questions

1. **AnimatedList vs simpler approach for completion animation**
   - What we know: `AnimatedList` provides proper `removeItem()` with animation, but requires imperative state management that can desync with Drift streams.
   - What's unclear: Whether the complexity of `AnimatedList` + stream synchronization is worth it vs a simpler approach (e.g., `AnimatedSwitcher` wrapping each item, or just letting items disappear on stream re-emission).
   - Recommendation: Use `AnimatedList` for overdue + today sections. On completion, call `removeItem()` for the slide-out animation, then let the stream naturally update. The stream re-emission after the animation completes will be a no-op if the item is already gone. Use a local copy of the list to avoid desync -- only update from stream when the local list is stale. This is manageable because the daily plan list is typically small (< 30 items).

2. **Progress count accuracy edge case: midnight rollover**
   - What we know: "Today" is `DateTime.now()` at query time. If the app stays open past midnight, "today" shifts.
   - What's unclear: Whether the daily plan should auto-refresh at midnight or require app restart.
   - Recommendation: Not critical for v1. The stream re-fires on any DB write. The user will see stale "today" data only if they leave the app open overnight without interacting. Acceptable for a household app.

## 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/home/` |
| Full suite command | `flutter test` |

### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| PLAN-01 | Cross-room query returns tasks with room names | unit | `flutter test test/features/home/data/daily_plan_dao_test.dart` | Wave 0 |
| PLAN-02 | Tasks with nextDueDate before today categorized as overdue | unit | `flutter test test/features/home/data/daily_plan_dao_test.dart` | Wave 0 |
| PLAN-03 | Tasks due tomorrow returned in upcoming list | unit | `flutter test test/features/home/data/daily_plan_dao_test.dart` | Wave 0 |
| PLAN-04 | Completing a task via DAO records completion and updates due date | unit | Already covered by `test/features/tasks/data/tasks_dao_test.dart` | Exists |
| PLAN-05 | Completions today count matches actual completions | unit | `flutter test test/features/home/data/daily_plan_dao_test.dart` | Wave 0 |
| PLAN-06 | Empty state shown when no overdue/today tasks exist | widget | `flutter test test/features/home/presentation/home_screen_test.dart` | Wave 0 |
| CLEAN-01 | Room card shows cleanliness indicator | unit | Already covered by `test/features/rooms/data/rooms_dao_test.dart` | Exists |

### Sampling Rate
- **Per task commit:** `flutter test test/features/home/`
- **Per wave merge:** `flutter test`
- **Phase gate:** Full suite green before `/gsd:verify-work`

### Wave 0 Gaps
- [ ] `test/features/home/data/daily_plan_dao_test.dart` -- covers PLAN-01, PLAN-02, PLAN-03, PLAN-05 (cross-room join query, date categorization, completion count)
- [ ] `test/features/home/presentation/home_screen_test.dart` -- covers PLAN-06 (empty state rendering) and basic section rendering
- [ ] No new framework dependencies needed; existing `flutter_test` + `drift` `NativeDatabase.memory()` pattern is sufficient

## Sources

### Primary (HIGH confidence)
- [Drift Select/Join docs](https://drift.simonbinder.eu/dart_api/select/) - Join query syntax, `readTable()`, `readTableOrNull()`, ordering on joins
- [Drift Stream docs](https://drift.simonbinder.eu/dart_api/streams/) - `.watch()` mechanism, table-level invalidation, stream behavior
- [Flutter AnimatedList API](https://api.flutter.dev/flutter/widgets/AnimatedList-class.html) - `removeItem()`, `GlobalKey<AnimatedListState>`, animation builder
- [Flutter AnimatedListState.removeItem](https://api.flutter.dev/flutter/widgets/AnimatedListState/removeItem.html) - Method signature, duration, builder pattern
- [Flutter ExpansionTile API](https://api.flutter.dev/flutter/material/ExpansionTile-class.html) - `initiallyExpanded`, Material 3 theming, `controlAffinity`
- [Flutter LinearProgressIndicator API](https://api.flutter.dev/flutter/material/LinearProgressIndicator-class.html) - Determinate mode with `value`, `minHeight`, Material 3 styling
- [Flutter M3 Progress Indicators Breaking Changes](https://docs.flutter.dev/release/breaking-changes/updated-material-3-progress-indicators) - `year2023` flag, new 2024 design spec
- Existing project codebase: `TasksDao`, `TaskRow`, `HomeScreen`, `RoomsDao`, `room_providers.dart`, `task_providers.dart`, `router.dart`

### Secondary (MEDIUM confidence)
- [Expansible in Flutter 3.32](https://himanshu-agarwal.medium.com/expansible-in-flutter-3-32-why-it-matters-how-to-use-it-727eeacb8dd2) - `ExpansibleController` deprecating `ExpansionTileController`
- [Riverpod combining providers](https://app.studyraid.com/en/read/12027/384445/combining-multiple-providers-for-complex-state-management) - `ref.watch` pattern for computed providers
- [Riverpod 3 stream alternatives](https://yfujiki.medium.com/an-alternative-of-stream-operation-in-riverpod-3-627a45f65140) - Stream combining without `.stream` access

### Tertiary (LOW confidence)
- None -- all findings verified with official docs or established project patterns

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - All libraries already in project, no new dependencies
- Architecture: HIGH - Patterns directly extend Phase 2 established conventions (DAO, StreamProvider, widget patterns), verified against existing code
- Data layer (join query): HIGH - Drift join documentation is clear and well-tested; project already uses Drift 2.31.0 with the exact same pattern available
- Animation (AnimatedList): MEDIUM - AnimatedList is well-documented but synchronization with reactive streams requires careful implementation. The desync pitfall is real but manageable at household-app scale.
- Pitfalls: HIGH - All identified pitfalls are based on established Flutter/Drift behavior verified in official docs

**Research date:** 2026-03-16
**Valid until:** 2026-04-16 (stable stack, no fast-moving dependencies)
