leafittome/lib/features/plants/presentation/plants_screen.dart
cschlaefke 1b2c880c89 V2: Haushalt teilen – Einladungscodes, Mitglieder- und Sitter-Rolle, sichtbare Bestätigungen
- Einladen per einmaligem 6-stelligem Code (7 Tage gültig), wahlweise als Mitglied oder Pflanzen-Sitter
- Beitritt über Cloud Function joinHousehold (Transaktion, Admin-Rechte)
- Security Rules: Sitter dürfen an Pflanzen nur Bestätigungs-Felder ändern; invites nur erstellen, nie lesen
- Haushalts-Screen: Mitgliederliste mit Rollen, Einladen, Beitreten mit Wechsel-Warnung
- UI-Gating: Sitter sehen keine Anlegen-/Bearbeiten-/Löschen-Aktionen
- Bestätigungen speichern den Namen (lastWateredBy/lastFertilizedBy), Anzeige im Profil
- Tests: Members-Map im Seed, neuer Sitter-Test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:14:34 +02:00

116 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/router/app_router.dart';
import '../../../core/widgets/app_drawer.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../household/data/household_providers.dart';
import '../../household/domain/household.dart';
import '../../locations/data/locations_provider.dart';
import '../data/plants_provider.dart';
import '../domain/plant.dart';
class PlantsScreen extends ConsumerWidget {
const PlantsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final plantsAsync = ref.watch(plantsProvider);
final plants = plantsAsync.value ?? const [];
final Widget body;
if (plantsAsync.isLoading && !plantsAsync.hasValue) {
body = const Center(child: CircularProgressIndicator());
} else if (plants.isEmpty) {
body = _EmptyView(l10n: l10n);
} else {
body = _plantList(context, ref, plants);
}
final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member;
return Scaffold(
appBar: AppBar(title: Text(l10n.plantsTitle)),
drawer: const AppDrawer(),
floatingActionButton: canEdit
? FloatingActionButton.extended(
onPressed: () => context.push(AppRoutes.plantAdd),
icon: const Icon(Icons.add),
label: Text(l10n.addPlant),
)
: null,
body: body,
);
}
Widget _plantList(BuildContext context, WidgetRef ref, List<Plant> plants) {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 96),
itemCount: plants.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final plant = plants[index];
final location =
ref.watch(locationByIdProvider(plant.locationId));
return ListTile(
leading: CircleAvatar(
radius: 24,
backgroundImage: plant.photoUrl != null
? NetworkImage(plant.photoUrl!)
: null,
child: plant.photoUrl == null
? const Icon(Icons.local_florist)
: null,
),
title: Text(
plant.nickname,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Text(
location != null
? '${plant.species}\n${location.name}'
: plant.species,
),
isThreeLine: location != null,
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push('/plants/${plant.id}'),
);
},
);
}
}
class _EmptyView extends StatelessWidget {
const _EmptyView({required this.l10n});
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.local_florist, size: 72),
const SizedBox(height: 16),
Text(
l10n.plantsEmpty,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.plantsEmptyHint,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
),
),
);
}
}