From b535f57a3984f607159a66f5f4f06594ecaff9cb Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 15 Mar 2026 22:09:42 +0100 Subject: [PATCH] feat(02-03): build task list screen with task row, completion, and overdue highlighting - TaskListScreen with room name in AppBar, edit/delete room actions - AsyncValue.when for loading/error/data states with empty state - TaskRow with leading checkbox, name, German relative due date, frequency label - Overdue dates highlighted in warm coral (0xFFE07A5F) - Checkbox marks done immediately (optimistic UI, no undo) - Row tap navigates to edit form, long-press shows delete confirmation - FAB for creating new tasks - ListView.builder for task list sorted by due date Co-Authored-By: Claude Opus 4.6 --- .../tasks/presentation/task_list_screen.dart | 223 +++++++++++++++++- lib/features/tasks/presentation/task_row.dart | 106 +++++++++ 2 files changed, 323 insertions(+), 6 deletions(-) create mode 100644 lib/features/tasks/presentation/task_row.dart diff --git a/lib/features/tasks/presentation/task_list_screen.dart b/lib/features/tasks/presentation/task_list_screen.dart index 8f05a68..c990875 100644 --- a/lib/features/tasks/presentation/task_list_screen.dart +++ b/lib/features/tasks/presentation/task_list_screen.dart @@ -1,17 +1,228 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; -/// Placeholder for the task list screen within a room. -/// Will be fully implemented in Plan 03. -class TaskListScreen extends StatelessWidget { +import 'package:household_keeper/core/database/database.dart'; +import 'package:household_keeper/core/providers/database_provider.dart'; +import 'package:household_keeper/features/tasks/presentation/task_providers.dart'; +import 'package:household_keeper/features/tasks/presentation/task_row.dart'; +import 'package:household_keeper/l10n/app_localizations.dart'; + +/// Screen displaying all tasks within a room, sorted by due date. +/// +/// Shows an empty state when no tasks exist, with a button to create one. +/// FAB always visible for quick task creation. +/// AppBar shows room name with edit and delete actions. +class TaskListScreen extends ConsumerWidget { const TaskListScreen({super.key, required this.roomId}); final int roomId; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final asyncTasks = ref.watch(tasksInRoomProvider(roomId)); + return Scaffold( - appBar: AppBar(title: const Text('Aufgaben')), - body: const Center(child: Text('Demnächst verfügbar')), + appBar: AppBar( + title: _RoomTitle(roomId: roomId), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () => context.go('/rooms/$roomId/edit'), + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _showRoomDeleteConfirmation(context, ref, l10n), + ), + ], + ), + body: asyncTasks.when( + data: (tasks) { + if (tasks.isEmpty) { + return _EmptyState(l10n: l10n, roomId: roomId); + } + return _TaskListView( + tasks: tasks, + l10n: l10n, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Fehler: $error'), + const SizedBox(height: 16), + FilledButton.tonal( + onPressed: () => ref.invalidate(tasksInRoomProvider(roomId)), + child: const Text('Erneut versuchen'), + ), + ], + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => context.go('/rooms/$roomId/tasks/new'), + child: const Icon(Icons.add), + ), + ); + } + + void _showRoomDeleteConfirmation( + BuildContext context, + WidgetRef ref, + AppLocalizations l10n, + ) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.roomDeleteConfirmTitle), + content: Text(l10n.roomDeleteConfirmMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(l10n.cancel), + ), + FilledButton( + onPressed: () { + Navigator.pop(ctx); + final db = ref.read(appDatabaseProvider); + db.roomsDao.deleteRoom(roomId); + // Navigate back to rooms list + context.go('/rooms'); + }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + child: Text(l10n.roomDeleteConfirmAction), + ), + ], + ), + ); + } +} + +/// Async room name loader for the AppBar title. +class _RoomTitle extends ConsumerWidget { + const _RoomTitle({required this.roomId}); + + final int roomId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Use a FutureBuilder-like approach: read room name from DB + return FutureBuilder( + future: ref.read(appDatabaseProvider).roomsDao.getRoomById(roomId), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data!.name); + } + return const Text('...'); + }, + ); + } +} + +/// Empty state shown when the room has no tasks. +class _EmptyState extends StatelessWidget { + const _EmptyState({required this.l10n, required this.roomId}); + + final AppLocalizations l10n; + final int roomId; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.task_alt, + size: 80, + color: theme.colorScheme.onSurface.withValues(alpha: 0.4), + ), + const SizedBox(height: 24), + Text( + l10n.taskEmptyTitle, + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + l10n.taskEmptyMessage, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + FilledButton.tonal( + onPressed: () => context.go('/rooms/$roomId/tasks/new'), + child: Text(l10n.taskEmptyAction), + ), + ], + ), + ), + ); + } +} + +/// List view of task rows with long-press delete support. +class _TaskListView extends ConsumerWidget { + const _TaskListView({required this.tasks, required this.l10n}); + + final List tasks; + final AppLocalizations l10n; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView.builder( + itemCount: tasks.length, + itemBuilder: (context, index) { + final task = tasks[index]; + return TaskRow( + key: ValueKey(task.id), + task: task, + onDelete: () => _showDeleteConfirmation(context, ref, task), + ); + }, + ); + } + + void _showDeleteConfirmation( + BuildContext context, + WidgetRef ref, + Task task, + ) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.taskDeleteConfirmTitle), + content: Text(l10n.taskDeleteConfirmMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(l10n.cancel), + ), + FilledButton( + onPressed: () { + Navigator.pop(ctx); + ref.read(taskActionsProvider.notifier).deleteTask(task.id); + }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + child: Text(l10n.taskDeleteConfirmAction), + ), + ], + ), ); } } diff --git a/lib/features/tasks/presentation/task_row.dart b/lib/features/tasks/presentation/task_row.dart new file mode 100644 index 0000000..4e3665b --- /dev/null +++ b/lib/features/tasks/presentation/task_row.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:household_keeper/core/database/database.dart'; +import 'package:household_keeper/features/tasks/domain/frequency.dart'; +import 'package:household_keeper/features/tasks/domain/relative_date.dart'; +import 'package:household_keeper/features/tasks/presentation/task_providers.dart'; + +/// Warm coral/terracotta color for overdue due date text. +const _overdueColor = Color(0xFFE07A5F); + +/// A single task row with leading checkbox, name, relative due date, +/// and frequency label. +/// +/// Per user decisions: +/// - Checkbox marks task done immediately (optimistic UI, no undo) +/// - Row tap opens edit form +/// - No swipe gesture +/// - No effort indicator or description preview on list view +/// - Overdue: due date text turns warm coral, rest stays normal +class TaskRow extends ConsumerWidget { + const TaskRow({ + super.key, + required this.task, + this.onDelete, + }); + + final Task task; + final VoidCallback? onDelete; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + + // Check if task is overdue (due date is before today) + final dueDate = DateTime( + task.nextDueDate.year, + task.nextDueDate.month, + task.nextDueDate.day, + ); + final isOverdue = dueDate.isBefore(today); + + // Format relative due date in German + final relativeDateText = formatRelativeDate(task.nextDueDate, now); + + // Build frequency label + final freq = FrequencyInterval( + intervalType: task.intervalType, + days: task.intervalDays, + ); + final frequencyText = freq.label(); + + return ListTile( + leading: Checkbox( + value: false, // Always unchecked -- completion is immediate + reschedule + onChanged: (_) { + // Mark done immediately (optimistic UI, no undo per user decision) + ref.read(taskActionsProvider.notifier).completeTask(task.id); + }, + ), + title: Text( + task.name, + style: theme.textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Row( + children: [ + Text( + relativeDateText, + style: theme.textTheme.bodySmall?.copyWith( + color: isOverdue ? _overdueColor : theme.colorScheme.onSurfaceVariant, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + '\u00b7', // middle dot separator + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + Flexible( + child: Text( + frequencyText, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + onTap: () { + // Tap row opens edit form + context.go('/rooms/${task.roomId}/tasks/${task.id}'); + }, + onLongPress: onDelete, + ); + } +}