# 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 (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 ## 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 | ## 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 with _$DailyPlanDaoMixin { DailyPlanDao(super.attachedDatabase); /// Watch all tasks joined with room name, sorted by nextDueDate ascending. Stream> 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 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 overdueTasks; final List todayTasks; final List 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((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 = []; final todayList = []; final tomorrowList = []; 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` 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( 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` 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 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> 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 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('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`, 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)