import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection.dart'; import '../../../../core/models/cotisation_model.dart'; import '../../../../shared/theme/app_theme.dart'; import '../../../../shared/widgets/buttons/buttons.dart'; import '../../../../shared/widgets/buttons/primary_button.dart'; import '../bloc/cotisations_bloc.dart'; import '../bloc/cotisations_event.dart'; import '../bloc/cotisations_state.dart'; import '../widgets/cotisation_card.dart'; import 'cotisation_detail_page.dart'; /// Page de recherche et filtrage des cotisations class CotisationsSearchPage extends StatefulWidget { const CotisationsSearchPage({super.key}); @override State createState() => _CotisationsSearchPageState(); } class _CotisationsSearchPageState extends State with TickerProviderStateMixin { late final CotisationsBloc _cotisationsBloc; late final TabController _tabController; late final AnimationController _animationController; final _searchController = TextEditingController(); final _scrollController = ScrollController(); String? _selectedStatut; String? _selectedType; int? _selectedAnnee; int? _selectedMois; bool _showAdvancedFilters = false; @override void initState() { super.initState(); _cotisationsBloc = getIt(); _tabController = TabController(length: 4, vsync: this); _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _scrollController.addListener(_onScroll); _animationController.forward(); } @override void dispose() { _searchController.dispose(); _scrollController.dispose(); _tabController.dispose(); _animationController.dispose(); super.dispose(); } void _onScroll() { if (_isBottom) { final currentState = _cotisationsBloc.state; if (currentState is CotisationsSearchResults && !currentState.hasReachedMax) { _performSearch(page: currentState.currentPage + 1); } } } bool get _isBottom { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; return currentScroll >= (maxScroll * 0.9); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _cotisationsBloc, child: Scaffold( backgroundColor: AppTheme.backgroundLight, appBar: AppBar( title: const Text('Recherche'), backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, bottom: TabBar( controller: _tabController, labelColor: Colors.white, unselectedLabelColor: Colors.white70, indicatorColor: Colors.white, tabs: const [ Tab(text: 'Toutes', icon: Icon(Icons.list)), Tab(text: 'En attente', icon: Icon(Icons.schedule)), Tab(text: 'En retard', icon: Icon(Icons.warning)), Tab(text: 'Payées', icon: Icon(Icons.check_circle)), ], onTap: (index) => _onTabChanged(index), ), ), body: Column( children: [ _buildSearchHeader(), if (_showAdvancedFilters) _buildAdvancedFilters(), Expanded( child: TabBarView( controller: _tabController, children: [ _buildSearchResults(), _buildSearchResults(statut: 'EN_ATTENTE'), _buildSearchResults(statut: 'EN_RETARD'), _buildSearchResults(statut: 'PAYEE'), ], ), ), ], ), ), ); } Widget _buildSearchHeader() { return Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0, 2), ), ], ), child: Column( children: [ // Barre de recherche TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher par nom, référence...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); _performSearch(); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), filled: true, fillColor: AppTheme.backgroundLight, ), onChanged: (value) { setState(() {}); _performSearch(); }, ), const SizedBox(height: 12), // Boutons d'action Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () { setState(() { _showAdvancedFilters = !_showAdvancedFilters; }); if (_showAdvancedFilters) { _animationController.forward(); } else { _animationController.reverse(); } }, icon: Icon(_showAdvancedFilters ? Icons.expand_less : Icons.tune), label: Text(_showAdvancedFilters ? 'Masquer filtres' : 'Filtres avancés'), ), ), const SizedBox(width: 12), OutlinedButton.icon( onPressed: _clearAllFilters, icon: const Icon(Icons.clear_all), label: const Text('Effacer'), ), ], ), ], ), ); } Widget _buildAdvancedFilters() { return AnimatedContainer( duration: const Duration(milliseconds: 300), height: _showAdvancedFilters ? null : 0, child: Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, border: Border( bottom: BorderSide(color: AppTheme.borderLight), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Filtres avancés', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 16), // Grille de filtres GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 3, children: [ _buildFilterDropdown( 'Type', _selectedType, ['Mensuelle', 'Annuelle', 'Exceptionnelle', 'Adhésion'], (value) => setState(() => _selectedType = value), ), _buildFilterDropdown( 'Année', _selectedAnnee?.toString(), List.generate(5, (i) => (DateTime.now().year - i).toString()), (value) => setState(() => _selectedAnnee = int.tryParse(value ?? '')), ), ], ), const SizedBox(height: 16), // Bouton d'application des filtres SizedBox( width: double.infinity, child: PrimaryButton( text: 'Appliquer les filtres', onPressed: _applyAdvancedFilters, ), ), ], ), ), ); } Widget _buildFilterDropdown( String label, String? value, List items, Function(String?) onChanged, ) { return DropdownButtonFormField( value: value, decoration: InputDecoration( labelText: label, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: [ DropdownMenuItem( value: null, child: Text('Tous les ${label.toLowerCase()}s'), ), ...items.map((item) => DropdownMenuItem( value: item, child: Text(item), )), ], onChanged: onChanged, ); } Widget _buildSearchResults({String? statut}) { return BlocBuilder( builder: (context, state) { if (state is CotisationsLoading) { return const Center(child: CircularProgressIndicator()); } if (state is CotisationsError) { return _buildErrorState(state); } if (state is CotisationsSearchResults) { final filteredResults = statut != null ? state.cotisations.where((c) => c.statut == statut).toList() : state.cotisations; if (filteredResults.isEmpty) { return _buildEmptyState(); } return RefreshIndicator( onRefresh: () async => _performSearch(refresh: true), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: filteredResults.length + (state.hasReachedMax ? 0 : 1), itemBuilder: (context, index) { if (index >= filteredResults.length) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } final cotisation = filteredResults[index]; return Padding( padding: const EdgeInsets.only(bottom: 12), child: CotisationCard( cotisation: cotisation, onTap: () => _navigateToDetail(cotisation), onPay: () => _navigateToDetail(cotisation), ), ); }, ), ); } return _buildInitialState(); }, ); } Widget _buildInitialState() { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search, size: 64, color: AppTheme.textHint, ), SizedBox(height: 16), Text( 'Recherchez des cotisations', style: TextStyle( fontSize: 18, color: AppTheme.textSecondary, ), ), SizedBox(height: 8), Text( 'Utilisez la barre de recherche ou les filtres', style: TextStyle( fontSize: 14, color: AppTheme.textHint, ), ), ], ), ); } Widget _buildEmptyState() { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search_off, size: 64, color: AppTheme.textHint, ), SizedBox(height: 16), Text( 'Aucun résultat trouvé', style: TextStyle( fontSize: 18, color: AppTheme.textSecondary, ), ), SizedBox(height: 8), Text( 'Essayez de modifier vos critères de recherche', style: TextStyle( fontSize: 14, color: AppTheme.textHint, ), ), ], ), ); } Widget _buildErrorState(CotisationsError state) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, size: 64, color: AppTheme.errorColor, ), const SizedBox(height: 16), Text( 'Erreur de recherche', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 8), Text( state.message, textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), const SizedBox(height: 24), PrimaryButton( text: 'Réessayer', onPressed: () => _performSearch(refresh: true), ), ], ), ); } // Actions void _onTabChanged(int index) { _performSearch(refresh: true); } void _performSearch({int page = 0, bool refresh = false}) { final query = _searchController.text.trim(); if (query.isEmpty && !_hasActiveFilters()) { return; } final filters = { if (query.isNotEmpty) 'query': query, if (_selectedStatut != null) 'statut': _selectedStatut, if (_selectedType != null) 'typeCotisation': _selectedType, if (_selectedAnnee != null) 'annee': _selectedAnnee, if (_selectedMois != null) 'mois': _selectedMois, }; _cotisationsBloc.add(ApplyAdvancedFilters(filters)); } void _applyAdvancedFilters() { _performSearch(refresh: true); } void _clearAllFilters() { setState(() { _searchController.clear(); _selectedStatut = null; _selectedType = null; _selectedAnnee = null; _selectedMois = null; }); _cotisationsBloc.add(const ResetCotisationsState()); } bool _hasActiveFilters() { return _selectedStatut != null || _selectedType != null || _selectedAnnee != null || _selectedMois != null; } void _navigateToDetail(CotisationModel cotisation) { Navigator.push( context, MaterialPageRoute( builder: (context) => CotisationDetailPage(cotisation: cotisation), ), ); } }