import 'package:cloud_functions/cloud_functions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.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/plant_diagnosis_service.dart'; import '../data/plants_provider.dart'; import '../domain/plant.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( 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, ), ), ], ), ), ), ], const Divider(height: 32), _DiagnosisSection(plant: plant), _DiagnosisHistory(plant: plant), ], ), ); } } /// Zeigt das Diagnose-Ergebnis als scrollbares Bottom-Sheet — sowohl für /// eine frische Untersuchung als auch für Einträge aus der Historie /// ([createdAt] gesetzt). Future _showDiagnosisSheet( BuildContext context, { required DiagnosisResult result, required String plantNickname, DateTime? createdAt, }) { final l10n = AppLocalizations.of(context); final locale = Localizations.localeOf(context).toString(); return showModalBottomSheet( context: context, isScrollControlled: true, builder: (sheetContext) { final theme = Theme.of(sheetContext); final healthyColor = theme.brightness == Brightness.dark ? Colors.green.shade300 : Colors.green.shade700; return DraggableScrollableSheet( expand: false, initialChildSize: 0.6, maxChildSize: 0.95, builder: (context, scrollController) => ListView( controller: scrollController, padding: const EdgeInsets.all(24), children: [ if (!result.matchesSpecies) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.errorContainer, borderRadius: BorderRadius.circular(12), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.swap_horiz, color: theme.colorScheme.onErrorContainer, ), const SizedBox(width: 12), Expanded( child: Text( l10n.diagnosisSpeciesMismatch(plantNickname), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onErrorContainer, ), ), ), ], ), ), const SizedBox(height: 16), ], Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( result.healthy ? Icons.check_circle : Icons.warning_amber_rounded, size: 32, color: result.healthy ? healthyColor : theme.colorScheme.error, ), const SizedBox(width: 12), Expanded( child: Text( result.summary, style: theme.textTheme.titleLarge, ), ), ], ), if (createdAt != null) ...[ const SizedBox(height: 8), Text( l10n.diagnosisDate( DateFormat('d. MMMM y, HH:mm', locale).format(createdAt), ), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], if (result.details.isNotEmpty) ...[ const SizedBox(height: 16), Text(result.details, style: theme.textTheme.bodyLarge), ], if (result.treatment.isNotEmpty) ...[ const SizedBox(height: 20), Text( l10n.diagnosisTreatmentTitle, style: theme.textTheme.titleMedium, ), const SizedBox(height: 4), Text(result.treatment, style: theme.textTheme.bodyLarge), ], if (result.prevention.isNotEmpty) ...[ const SizedBox(height: 20), Text( l10n.diagnosisPreventionTitle, style: theme.textTheme.titleMedium, ), const SizedBox(height: 4), Text(result.prevention, style: theme.textTheme.bodyLarge), ], const SizedBox(height: 16), Text( l10n.diagnosisDisclaimer, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ); }, ); } /// Liste der gespeicherten Untersuchungen dieser Pflanze (neueste zuerst). class _DiagnosisHistory extends ConsumerWidget { const _DiagnosisHistory({required this.plant}); final Plant plant; @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final entries = ref.watch(plantDiagnosesProvider(plant.id)).value ?? []; if (entries.isEmpty) return const SizedBox.shrink(); final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member; final locale = Localizations.localeOf(context).toString(); final dateFormat = DateFormat('d. MMMM y, HH:mm', locale); final healthyColor = theme.brightness == Brightness.dark ? Colors.green.shade300 : Colors.green.shade700; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 24), Text(l10n.diagnosisHistoryTitle, style: theme.textTheme.titleMedium), for (final entry in entries) ListTile( contentPadding: EdgeInsets.zero, leading: Icon( entry.result.healthy ? Icons.check_circle : Icons.warning_amber_rounded, color: entry.result.healthy ? healthyColor : theme.colorScheme.error, ), title: Text( entry.result.summary, maxLines: 2, overflow: TextOverflow.ellipsis, ), subtitle: entry.createdAt != null ? Text(dateFormat.format(entry.createdAt!)) : null, trailing: canEdit ? IconButton( icon: const Icon(Icons.delete_outline), tooltip: l10n.delete, onPressed: () => _confirmDelete(context, ref, entry), ) : null, onTap: () => _showDiagnosisSheet( context, result: entry.result, plantNickname: plant.nickname, createdAt: entry.createdAt, ), ), ], ); } Future _confirmDelete( BuildContext context, WidgetRef ref, PlantDiagnosisEntry entry, ) async { final l10n = AppLocalizations.of(context); final locale = Localizations.localeOf(context).toString(); final dateText = entry.createdAt != null ? DateFormat('d. MMMM y', locale).format(entry.createdAt!) : ''; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(l10n.deleteDiagnosisQuestion(dateText)), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: Text(l10n.cancel), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(l10n.delete), ), ], ), ); if (confirmed == true) { await ref .read(plantDiagnosisServiceProvider) .deleteDiagnosis(plant.id, entry.id); } } } /// Krankheits-Check (V3): Foto aufnehmen → Claude beurteilt den Zustand /// und gibt Behandlungstipps. Auch für Sitter nutzbar (nur lesend). class _DiagnosisSection extends ConsumerStatefulWidget { const _DiagnosisSection({required this.plant}); final Plant plant; @override ConsumerState<_DiagnosisSection> createState() => _DiagnosisSectionState(); } class _DiagnosisSectionState extends ConsumerState<_DiagnosisSection> { final _picker = ImagePicker(); bool _busy = false; Future _start() async { final l10n = AppLocalizations.of(context); final source = await showModalBottomSheet( context: context, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.photo_camera), title: Text(l10n.takePhoto), onTap: () => Navigator.pop(sheetContext, ImageSource.camera), ), ListTile( leading: const Icon(Icons.photo_library), title: Text(l10n.fromGallery), onTap: () => Navigator.pop(sheetContext, ImageSource.gallery), ), ], ), ), ); if (source == null || !mounted) return; final picked = await _picker.pickImage( source: source, maxWidth: 1280, imageQuality: 80, ); if (picked == null || !mounted) return; final bytes = await picked.readAsBytes(); setState(() => _busy = true); try { final service = ref.read(plantDiagnosisServiceProvider); final result = await service.diagnose( bytes, speciesHint: widget.plant.species, ); // In der Historie ablegen — ein Speicherfehler soll die Anzeige des // Ergebnisses aber nicht verhindern. try { await service.saveDiagnosis(widget.plant.id, result); } catch (error) { debugPrint('Untersuchung speichern fehlgeschlagen: $error'); } if (!mounted) return; await _showDiagnosisSheet( context, result: result, plantNickname: widget.plant.nickname, ); } on FirebaseFunctionsException catch (e) { if (!mounted) return; // Die Function liefert deutsche Meldungen (z. B. „keine Pflanze // erkannt") – die zeigen wir direkt an. ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text(e.message ?? l10n.diagnosisFailed), )); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.diagnosisFailed))); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ OutlinedButton.icon( onPressed: _busy ? null : _start, icon: _busy ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.health_and_safety_outlined), label: Text(_busy ? l10n.diagnosing : l10n.diagnoseButton), ), const SizedBox(height: 4), Text( l10n.diagnoseHint, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ], ); } } 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, ), ), ], ), ), ], ), ); } }