leafittome/lib/features/plants/presentation/plant_detail_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

193 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../core/router/app_router.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';
class PlantDetailScreen extends ConsumerWidget {
const PlantDetailScreen({super.key, required this.plantId});
final String plantId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final plant = ref.watch(plantByIdProvider(plantId));
if (plant == null) {
// Pflanze wurde gelöscht, während der Screen offen war.
return Scaffold(appBar: AppBar(), body: const SizedBox.shrink());
}
final location = ref.watch(locationByIdProvider(plant.locationId));
final locale = Localizations.localeOf(context).toString();
final dateFormat = DateFormat('d. MMMM y', locale);
String formatDate(DateTime? date) =>
date != null ? dateFormat.format(date) : l10n.never;
final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member;
return Scaffold(
appBar: AppBar(
title: Text(plant.nickname),
actions: [
if (canEdit) ...[
IconButton(
icon: const Icon(Icons.edit),
tooltip: l10n.plantDetailTitle,
onPressed: () => context.push(AppRoutes.plantEdit, extra: plant),
),
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: l10n.delete,
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.deletePlantQuestion(plant.nickname)),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(l10n.delete),
),
],
),
);
if (confirmed == true && context.mounted) {
ref.read(plantRepositoryProvider).removePlant(plant.id);
context.pop();
}
},
),
],
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
if (plant.photoUrl != null) ...[
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
plant.photoUrl!,
height: 220,
fit: BoxFit.cover,
),
),
const SizedBox(height: 16),
],
_InfoTile(
icon: Icons.local_florist,
label: l10n.speciesLabel,
value: plant.species,
),
_InfoTile(
icon: Icons.place,
label: l10n.locationLabel,
value: location?.name ?? l10n.locationNone,
),
const Divider(height: 32),
_InfoTile(
icon: Icons.water_drop,
label: l10n.wateringEvery(plant.wateringIntervalDays),
value: l10n.lastWatered(formatDate(plant.lastWatered)) +
(plant.lastWateredBy != null
? l10n.doneBy(plant.lastWateredBy!)
: ''),
),
_InfoTile(
icon: Icons.compost,
label: l10n.fertilizingEvery(plant.fertilizingIntervalDays),
value: l10n.lastFertilized(formatDate(plant.lastFertilized)) +
(plant.lastFertilizedBy != null
? l10n.doneBy(plant.lastFertilizedBy!)
: ''),
),
if (plant.description.isNotEmpty) ...[
const Divider(height: 32),
Text(
plant.description,
style: Theme.of(context).textTheme.bodyLarge,
),
],
if (plant.careNotes.isNotEmpty) ...[
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.tips_and_updates,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Text(
plant.careNotes,
style: Theme.of(context).textTheme.bodyLarge,
),
),
],
),
),
),
],
],
),
);
}
}
class _InfoTile extends StatelessWidget {
const _InfoTile({
required this.icon,
required this.label,
required this.value,
});
final IconData icon;
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: theme.colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: theme.textTheme.titleMedium),
const SizedBox(height: 2),
Text(
value,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
);
}
}