import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import '../../../core/constants/design_system.dart'; import '../../../data/datasources/establishment_remote_data_source.dart'; import '../../../domain/entities/establishment.dart'; import '../../widgets/animated_widgets.dart'; import '../../widgets/custom_snackbar.dart'; import '../../widgets/modern_empty_state.dart'; import '../../widgets/shimmer_loading.dart'; /// Écran des établissements avec recherche et filtres. class EstablishmentsScreen extends StatefulWidget { const EstablishmentsScreen({super.key}); @override State createState() => _EstablishmentsScreenState(); } class _EstablishmentsScreenState extends State { final EstablishmentRemoteDataSource _dataSource = EstablishmentRemoteDataSource(http.Client()); final TextEditingController _searchController = TextEditingController(); List _establishments = []; List _filteredEstablishments = []; bool _isLoading = false; String? _errorMessage; // Filtres EstablishmentType? _selectedType; PriceRange? _selectedPriceRange; String? _selectedCity; final List _availableCities = []; @override void initState() { super.initState(); _loadEstablishments(); _searchController.addListener(_onSearchChanged); } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _loadEstablishments() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final models = await _dataSource.getAllEstablishments(); if (mounted) { setState(() { _establishments = models.map((m) => m.toEntity()).toList(); _filteredEstablishments = _establishments; // Extraire les villes uniques final cities = _establishments.map((e) => e.city).toSet().toList(); _availableCities.clear(); _availableCities.addAll(cities); _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _errorMessage = e.toString(); _isLoading = false; }); } } } void _onSearchChanged() { _applyFilters(); } void _applyFilters() { final query = _searchController.text.toLowerCase(); setState(() { _filteredEstablishments = _establishments.where((establishment) { // Filtre par recherche (nom ou ville) final matchesSearch = query.isEmpty || establishment.name.toLowerCase().contains(query) || establishment.city.toLowerCase().contains(query); // Filtre par type final matchesType = _selectedType == null || establishment.type == _selectedType; // Filtre par prix final matchesPrice = _selectedPriceRange == null || establishment.priceRange == _selectedPriceRange; // Filtre par ville final matchesCity = _selectedCity == null || establishment.city == _selectedCity; return matchesSearch && matchesType && matchesPrice && matchesCity; }).toList(); }); } void _clearFilters() { setState(() { _selectedType = null; _selectedPriceRange = null; _selectedCity = null; _searchController.clear(); _applyFilters(); }); } Future _showFilters() async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _FiltersBottomSheet( selectedType: _selectedType, selectedPriceRange: _selectedPriceRange, selectedCity: _selectedCity, availableCities: _availableCities, onApply: (type, priceRange, city) { setState(() { _selectedType = type; _selectedPriceRange = priceRange; _selectedCity = city; }); _applyFilters(); }, ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final hasActiveFilters = _selectedType != null || _selectedPriceRange != null || _selectedCity != null; return Scaffold( body: Column( children: [ // Barre de recherche et filtres Container( padding: const EdgeInsets.all(DesignSystem.spacingLg), child: Column( children: [ // Champ de recherche TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher un établissement...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () => _searchController.clear(), ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), filled: true, ), ), const SizedBox(height: DesignSystem.spacingSm), // Boutons de filtres Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _showFilters, icon: Icon( Icons.filter_list, color: hasActiveFilters ? theme.colorScheme.primary : null, ), label: Text( hasActiveFilters ? 'Filtres actifs' : 'Filtres', style: TextStyle( color: hasActiveFilters ? theme.colorScheme.primary : null, fontWeight: hasActiveFilters ? FontWeight.bold : null, ), ), style: OutlinedButton.styleFrom( side: BorderSide( color: hasActiveFilters ? theme.colorScheme.primary : theme.dividerColor, width: hasActiveFilters ? 2 : 1, ), ), ), ), if (hasActiveFilters) ...[ const SizedBox(width: DesignSystem.spacingSm), IconButton( onPressed: _clearFilters, icon: const Icon(Icons.clear_all), tooltip: 'Effacer les filtres', ), ], ], ), ], ), ), // Liste des établissements Expanded( child: _isLoading ? const SkeletonList( itemCount: 6, skeletonWidget: ListItemSkeleton(), ) : _errorMessage != null ? _buildErrorState(theme) : _filteredEstablishments.isEmpty ? _buildEmptyState() : RefreshIndicator( onRefresh: _loadEstablishments, child: ListView.builder( padding: const EdgeInsets.all(DesignSystem.spacingLg), itemCount: _filteredEstablishments.length, itemBuilder: (context, index) { return _buildEstablishmentCard( _filteredEstablishments[index], ); }, ), ), ), ], ), ); } Widget _buildErrorState(ThemeData theme) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), const SizedBox(height: DesignSystem.spacingLg), Text('Erreur de chargement', style: theme.textTheme.titleLarge), const SizedBox(height: DesignSystem.spacingSm), Text(_errorMessage!, style: theme.textTheme.bodyMedium, textAlign: TextAlign.center), const SizedBox(height: DesignSystem.spacingXl), ElevatedButton.icon( onPressed: _loadEstablishments, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), ), ], ), ); } Widget _buildEmptyState() { return ModernEmptyState( illustration: EmptyStateIllustration.search, title: 'Aucun établissement trouvé', description: _searchController.text.isNotEmpty || (_selectedType != null || _selectedPriceRange != null || _selectedCity != null) ? 'Essayez de modifier vos critères de recherche' : 'Aucun établissement disponible pour le moment', ); } Widget _buildEstablishmentCard(Establishment establishment) { final theme = Theme.of(context); return FadeInWidget( child: AnimatedCard( margin: const EdgeInsets.only(bottom: DesignSystem.spacingLg), onTap: () { // TODO: Naviguer vers les détails context.showInfo('Détails de ${establishment.name}'); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image if (establishment.imageUrl != null) ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(DesignSystem.radiusMd), ), child: AspectRatio( aspectRatio: 16 / 9, child: Image.network( establishment.imageUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( color: Colors.grey[300], child: const Icon(Icons.location_city, size: 48), ); }, ), ), ), Padding( padding: const EdgeInsets.all(DesignSystem.spacingLg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nom et type Row( children: [ Text( establishment.type.icon, style: const TextStyle(fontSize: 24), ), const SizedBox(width: DesignSystem.spacingSm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( establishment.name, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), Text( establishment.type.displayName, style: theme.textTheme.bodySmall?.copyWith( color: Colors.grey[600], ), ), ], ), ), if (establishment.priceRange != null) Text( establishment.priceRange!.symbol, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), ], ), const SizedBox(height: DesignSystem.spacingSm), // Adresse Row( children: [ const Icon(Icons.location_on, size: 16, color: Colors.grey), const SizedBox(width: 4), Expanded( child: Text( establishment.fullAddress, style: theme.textTheme.bodySmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), // Note if (establishment.rating != null) ...[ const SizedBox(height: DesignSystem.spacingSm), Row( children: [ const Icon(Icons.star, size: 16, color: Colors.amber), const SizedBox(width: 4), Text( establishment.rating!.toStringAsFixed(1), style: theme.textTheme.bodySmall?.copyWith( fontWeight: FontWeight.bold, ), ), Text( ' / 5.0', style: theme.textTheme.bodySmall?.copyWith( color: Colors.grey[600], ), ), ], ), ], ], ), ), ], ), ), ); } } /// Bottom sheet pour les filtres class _FiltersBottomSheet extends StatefulWidget { const _FiltersBottomSheet({ required this.selectedType, required this.selectedPriceRange, required this.selectedCity, required this.availableCities, required this.onApply, }); final EstablishmentType? selectedType; final PriceRange? selectedPriceRange; final String? selectedCity; final List availableCities; final void Function(EstablishmentType?, PriceRange?, String?) onApply; @override State<_FiltersBottomSheet> createState() => _FiltersBottomSheetState(); } class _FiltersBottomSheetState extends State<_FiltersBottomSheet> { late EstablishmentType? _type; late PriceRange? _priceRange; late String? _city; @override void initState() { super.initState(); _type = widget.selectedType; _priceRange = widget.selectedPriceRange; _city = widget.selectedCity; } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(DesignSystem.radiusLg)), ), padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom + DesignSystem.spacingLg, top: DesignSystem.spacingLg, left: DesignSystem.spacingLg, right: DesignSystem.spacingLg, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Handle bar Center( child: Container( width: 40, height: 4, margin: const EdgeInsets.only(bottom: DesignSystem.spacingLg), decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), ), Text('Filtres', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: DesignSystem.spacingXl), // Type Text('Type d\'établissement', style: theme.textTheme.titleSmall), const SizedBox(height: DesignSystem.spacingSm), Wrap( spacing: DesignSystem.spacingSm, runSpacing: DesignSystem.spacingSm, children: EstablishmentType.values.map((type) { final isSelected = _type == type; return FilterChip( label: Text(type.displayName), selected: isSelected, onSelected: (selected) { setState(() { _type = selected ? type : null; }); }, ); }).toList(), ), const SizedBox(height: DesignSystem.spacingXl), // Prix Text('Fourchette de prix', style: theme.textTheme.titleSmall), const SizedBox(height: DesignSystem.spacingSm), Wrap( spacing: DesignSystem.spacingSm, children: PriceRange.values.map((price) { final isSelected = _priceRange == price; return FilterChip( label: Text(price.symbol), selected: isSelected, onSelected: (selected) { setState(() { _priceRange = selected ? price : null; }); }, ); }).toList(), ), const SizedBox(height: DesignSystem.spacingXl), // Ville if (widget.availableCities.isNotEmpty) ...[ Text('Ville', style: theme.textTheme.titleSmall), const SizedBox(height: DesignSystem.spacingSm), DropdownButtonFormField( value: _city, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), contentPadding: const EdgeInsets.symmetric(horizontal: DesignSystem.spacingLg, vertical: DesignSystem.spacingSm), ), hint: const Text('Toutes les villes'), items: widget.availableCities.map((city) { return DropdownMenuItem(value: city, child: Text(city)); }).toList(), onChanged: (value) { setState(() { _city = value; }); }, ), const SizedBox(height: DesignSystem.spacingXl), ], // Boutons Row( children: [ Expanded( child: OutlinedButton( onPressed: () { setState(() { _type = null; _priceRange = null; _city = null; }); }, child: const Text('Réinitialiser'), ), ), const SizedBox(width: DesignSystem.spacingLg), Expanded( child: ElevatedButton( onPressed: () { widget.onApply(_type, _priceRange, _city); Navigator.pop(context); }, child: const Text('Appliquer'), ), ), ], ), ], ), ); } }