import 'package:cloud_functions/cloud_functions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import '../../../l10n/generated/app_localizations.dart'; import '../../household/data/household_providers.dart'; import '../../household/domain/household.dart'; import '../../plants/data/plant_fit_service.dart'; import '../../plants/data/plants_provider.dart'; import '../../plants/domain/plant.dart'; import '../data/location_analysis_service.dart'; import '../data/locations_provider.dart'; import '../domain/plant_location.dart'; class LocationDetailScreen extends ConsumerWidget { const LocationDetailScreen({super.key, required this.locationId}); final String locationId; @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final location = ref.watch(locationByIdProvider(locationId)); if (location == null) { // Stellplatz wurde gelöscht, während der Screen offen war. return Scaffold(appBar: AppBar(), body: const SizedBox.shrink()); } final plants = (ref.watch(plantsProvider).value ?? const []) .where((plant) => plant.locationId == locationId) .toList(); final canEdit = ref.watch(myRoleProvider) == HouseholdRole.member; return Scaffold( appBar: AppBar(title: Text(location.name)), body: ListView( padding: const EdgeInsets.all(16), children: [ _LocationPhotoSection(location: location, canEdit: canEdit), const Divider(height: 32), Text( l10n.locationAssignedPlants, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), if (plants.isEmpty) Text( l10n.locationNoPlantsAssigned, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ) else for (final plant in plants) ...[ _PlantFitTile(plant: plant, location: location), const Divider(height: 24), ], ], ), ); } } /// Foto vom Stellplatz aufnehmen und von Claude die Lichtverhältnisse /// einschätzen lassen (V3). Nur volle Mitglieder dürfen das auslösen. class _LocationPhotoSection extends ConsumerStatefulWidget { const _LocationPhotoSection({required this.location, required this.canEdit}); final PlantLocation location; final bool canEdit; @override ConsumerState<_LocationPhotoSection> createState() => _LocationPhotoSectionState(); } class _LocationPhotoSectionState extends ConsumerState<_LocationPhotoSection> { final _picker = ImagePicker(); bool _busy = false; Future _pickAndAnalyze(ImageSource source) async { final l10n = AppLocalizations.of(context); 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(locationAnalysisServiceProvider); final photoUrl = await service.uploadPhoto(bytes); final result = await service.analyze(bytes); await ref.read(locationRepositoryProvider).updateAnalysis( widget.location.id, photoUrl: photoUrl, lightCategory: result.lightCategory, lightAssessment: result.description, ); } on FirebaseFunctionsException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(e.message ?? l10n.locationAnalysisFailed))); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.locationAnalysisFailed))); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final location = widget.location; Widget preview; if (location.photoUrl != null) { preview = Image.network(location.photoUrl!, fit: BoxFit.cover); } else { preview = Container( color: theme.colorScheme.surfaceContainerHighest, child: Icon( Icons.place, size: 64, color: theme.colorScheme.onSurfaceVariant, ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ClipRRect( borderRadius: BorderRadius.circular(16), child: SizedBox( height: 180, child: Stack( fit: StackFit.expand, children: [ preview, if (_busy) Container( color: Colors.black45, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: Colors.white), const SizedBox(height: 12), Text( l10n.locationAnalyzing, style: const TextStyle(color: Colors.white), ), ], ), ), ], ), ), ), const SizedBox(height: 12), if (widget.canEdit) Row( children: [ Expanded( child: FilledButton.tonalIcon( onPressed: _busy ? null : () => _pickAndAnalyze(ImageSource.camera), icon: const Icon(Icons.photo_camera), label: Text(l10n.takePhoto), ), ), const SizedBox(width: 12), Expanded( child: FilledButton.tonalIcon( onPressed: _busy ? null : () => _pickAndAnalyze(ImageSource.gallery), icon: const Icon(Icons.photo_library), label: Text(l10n.fromGallery), ), ), ], ), const SizedBox(height: 12), if (location.lightCategory != null) ...[ Chip(label: Text(location.lightCategory!)), if (location.lightAssessment != null) ...[ const SizedBox(height: 8), Text( location.lightAssessment!, style: theme.textTheme.bodyMedium, ), ], ] else Text( l10n.locationAnalysisEmpty, style: theme.textTheme.bodyMedium ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), ], ); } } /// Eine zugeordnete Pflanze mit ihrer Eignungs-Bewertung (auf Abruf) oder /// einem Button, um sie anzustoßen. Für alle Rollen nutzbar (informativ), /// setzt aber eine bereits analysierte Standort-Analyse voraus. class _PlantFitTile extends ConsumerStatefulWidget { const _PlantFitTile({required this.plant, required this.location}); final Plant plant; final PlantLocation location; @override ConsumerState<_PlantFitTile> createState() => _PlantFitTileState(); } class _PlantFitTileState extends ConsumerState<_PlantFitTile> { bool _busy = false; Future _check() async { final l10n = AppLocalizations.of(context); setState(() => _busy = true); try { final result = await ref.read(plantFitServiceProvider).assessFit( plant: widget.plant, location: widget.location, ); await ref.read(plantRepositoryProvider).saveFitAssessment( widget.plant.id, stars: result.stars, reasoning: result.reasoning, locationId: widget.location.id, ); } on FirebaseFunctionsException catch (e) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(e.message ?? l10n.fitCheckFailed))); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(l10n.fitCheckFailed))); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final plant = widget.plant; final canAnalyze = widget.location.lightCategory != null; final stale = isFitStale(plant, widget.location); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(plant.nickname, style: theme.textTheme.titleMedium), if (plant.fitStars != null && !stale) ...[ const SizedBox(height: 2), Text( '★' * plant.fitStars! + '☆' * (5 - plant.fitStars!), style: theme.textTheme.bodyLarge, ), if (plant.fitReasoning != null) Text( plant.fitReasoning!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ] else if (plant.fitStars != null && stale) ...[ const SizedBox(height: 2), Text( l10n.fitCheckStale, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ], ), ), const SizedBox(width: 12), OutlinedButton.icon( onPressed: _busy || !canAnalyze ? null : _check, icon: _busy ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.star_outline, size: 18), label: Text( _busy ? l10n.fitChecking : !canAnalyze ? l10n.fitCheckNeedsAnalysis : l10n.fitCheckButton, ), ), ], ), ); } }