Files
HouseHoldKeaper/lib/features/rooms/presentation/rooms_screen.dart
Jean-Luc Makiola 519a56bef7 feat(02-02): build rooms screen with reorderable card grid and room card
- Replace placeholder RoomsScreen with ConsumerWidget watching roomWithStatsProvider
- Create RoomCard with icon, name, due count badge, cleanliness progress bar
- 2-column ReorderableBuilder grid with drag-and-drop reorder
- Empty state, loading, error states with retry
- Long-press menu for edit/delete with confirmation dialog
- FAB for room creation navigation
- Update app_shell_test with provider override for rooms stream

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:07:09 +01:00

189 lines
5.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:household_keeper/features/rooms/data/rooms_dao.dart';
import 'package:household_keeper/features/rooms/presentation/room_card.dart';
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
import 'package:household_keeper/l10n/app_localizations.dart';
/// Rooms screen showing a 2-column reorderable card grid of rooms.
///
/// Displays an empty state when no rooms exist, with a button to create one.
/// FAB always visible for quick room creation.
class RoomsScreen extends ConsumerWidget {
const RoomsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final asyncRooms = ref.watch(roomWithStatsListProvider);
return Scaffold(
body: asyncRooms.when(
data: (rooms) {
if (rooms.isEmpty) {
return _EmptyState(l10n: l10n);
}
return _RoomGrid(rooms: rooms);
},
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(roomWithStatsListProvider),
child: const Text('Erneut versuchen'),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.go('/rooms/new'),
child: const Icon(Icons.add),
),
);
}
}
/// Empty state shown when no rooms have been created yet.
class _EmptyState extends StatelessWidget {
const _EmptyState({required this.l10n});
final AppLocalizations l10n;
@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.door_front_door_rounded,
size: 80,
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
const SizedBox(height: 24),
Text(
l10n.roomsEmptyTitle,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.roomsEmptyMessage,
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/new'),
child: Text(l10n.roomsEmptyAction),
),
],
),
),
);
}
}
/// 2-column reorderable grid of room cards.
class _RoomGrid extends ConsumerStatefulWidget {
const _RoomGrid({required this.rooms});
final List<RoomWithStats> rooms;
@override
ConsumerState<_RoomGrid> createState() => _RoomGridState();
}
class _RoomGridState extends ConsumerState<_RoomGrid> {
final _scrollController = ScrollController();
final _gridViewKey = GlobalKey();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final children = widget.rooms.map((rws) {
return RoomCard(
key: ValueKey(rws.room.id),
roomWithStats: rws,
onEdit: () => context.go('/rooms/${rws.room.id}/edit'),
onDelete: () => _showDeleteConfirmation(context, rws, l10n),
);
}).toList();
return ReorderableBuilder<Widget>(
scrollController: _scrollController,
onReorder: (ReorderedListFunction<Widget> reorderFunc) {
final reordered = reorderFunc(children);
final newOrder = reordered
.map((w) => (w.key! as ValueKey<int>).value)
.toList();
ref.read(roomActionsProvider.notifier).reorderRooms(newOrder);
},
builder: (reorderableChildren) {
return GridView.count(
key: _gridViewKey,
controller: _scrollController,
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.0,
children: reorderableChildren,
);
},
children: children,
);
}
void _showDeleteConfirmation(
BuildContext context,
RoomWithStats rws,
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);
ref.read(roomActionsProvider.notifier).deleteRoom(rws.room.id);
},
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
child: Text(l10n.roomDeleteConfirmAction),
),
],
),
);
}
}