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>
This commit is contained in:
2026-03-15 22:07:09 +01:00
parent 32e61e4bec
commit 519a56bef7
3 changed files with 290 additions and 25 deletions

View File

@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:household_keeper/features/rooms/data/rooms_dao.dart';
import 'package:household_keeper/features/rooms/domain/room_icons.dart';
import 'package:household_keeper/l10n/app_localizations.dart';
/// A card widget displaying room info: icon, name, due task count, and
/// a thin cleanliness progress bar at the bottom.
class RoomCard extends StatelessWidget {
const RoomCard({
super.key,
required this.roomWithStats,
this.onEdit,
this.onDelete,
});
final RoomWithStats roomWithStats;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context);
final room = roomWithStats.room;
// Green -> Red based on cleanliness ratio
const cleanColor = Color(0xFF7A9A6D); // Sage green (clean)
const dirtyColor = Color(0xFFE07A5F); // Warm coral (overdue)
final barColor = Color.lerp(
dirtyColor,
cleanColor,
roomWithStats.cleanlinessRatio,
)!;
return Card(
color: theme.colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 0.5,
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => context.go('/rooms/${room.id}'),
onLongPress: () => _showMenu(context, l10n),
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
mapIconName(room.iconName),
size: 36,
color: theme.colorScheme.primary,
),
const SizedBox(height: 8),
Text(
room.name,
style: theme.textTheme.titleSmall,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (roomWithStats.dueTasks > 0) ...[
const SizedBox(height: 4),
Text(
l10n.roomCardDueCount(roomWithStats.dueTasks),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
],
],
),
),
),
// Thin cleanliness bar at bottom
LinearProgressIndicator(
value: roomWithStats.cleanlinessRatio,
backgroundColor: theme.colorScheme.surfaceContainerHighest,
color: barColor,
minHeight: 3,
),
],
),
),
);
}
void _showMenu(BuildContext context, AppLocalizations l10n) {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.edit),
title: Text(l10n.roomFormEditTitle),
onTap: () {
Navigator.pop(context);
onEdit?.call();
},
),
ListTile(
leading: Icon(Icons.delete, color: Theme.of(context).colorScheme.error),
title: Text(
l10n.roomDeleteConfirmAction,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onTap: () {
Navigator.pop(context);
onDelete?.call();
},
),
],
),
),
);
}
}