From a8552538ec38d127f77dd1063fc9af15cf6e44aa Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 12:14:47 +0100 Subject: [PATCH] docs(phase-3): research daily plan and cleanliness domain --- .../03-RESEARCH.md | 695 ++++++++++++++++++ 1 file changed, 695 insertions(+) create mode 100644 .planning/phases/03-daily-plan-and-cleanliness/03-RESEARCH.md diff --git a/.planning/phases/03-daily-plan-and-cleanliness/03-RESEARCH.md b/.planning/phases/03-daily-plan-and-cleanliness/03-RESEARCH.md new file mode 100644 index 0000000..9620063 --- /dev/null +++ b/.planning/phases/03-daily-plan-and-cleanliness/03-RESEARCH.md @@ -0,0 +1,695 @@ +# 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)