import 'package:flutter/material.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../shared/design_system/tokens/app_colors.dart'; import '../../../../shared/design_system/tokens/module_colors.dart'; import '../../../../shared/design_system/components/uf_app_bar.dart'; import '../../../../shared/models/membre_search_criteria.dart'; import '../../../../shared/models/membre_search_result.dart'; import '../../../organizations/domain/repositories/organization_repository.dart'; import '../../../organizations/data/models/organization_model.dart'; import '../../data/services/membre_search_service.dart'; import '../widgets/membre_search_results.dart'; import '../widgets/search_statistics_card.dart'; /// Page de recherche avancée des membres /// Interface complète pour la recherche sophistiquée avec filtres multiples class AdvancedSearchPage extends StatefulWidget { const AdvancedSearchPage({super.key}); @override State createState() => _AdvancedSearchPageState(); } class _AdvancedSearchPageState extends State with TickerProviderStateMixin { late TabController _tabController; late final MembreSearchService _searchService = sl(); MembreSearchCriteria _currentCriteria = MembreSearchCriteria.empty; List _organisations = []; bool _organisationsLoaded = false; MembreSearchResult? _currentResult; bool _isSearching = false; String? _errorMessage; // Contrôleurs pour les champs de recherche final _queryController = TextEditingController(); final _nomController = TextEditingController(); final _prenomController = TextEditingController(); final _emailController = TextEditingController(); final _telephoneController = TextEditingController(); final _regionController = TextEditingController(); final _villeController = TextEditingController(); final _professionController = TextEditingController(); // Valeurs pour les filtres String? _selectedStatut; final List _selectedRoles = []; final List _selectedOrganisations = []; RangeValues _ageRange = const RangeValues(18, 65); bool _includeInactifs = false; bool _membreBureau = false; bool _responsable = false; /// Rôles Keycloak utilisables pour le filtre (noms envoyés à l'API) static const List _searchRoleCodes = [ 'MEMBRE_ACTIF', 'MEMBRE_SIMPLE', 'ADMIN_ORGANISATION', 'SECRETAIRE', 'TRESORIER', 'CONSULTANT', 'GESTIONNAIRE_RH', 'SUPER_ADMINISTRATEUR', ]; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _loadOrganisations(); } static String _roleDisplayName(String code) { const labels = { 'MEMBRE_ACTIF': 'Membre actif', 'MEMBRE_SIMPLE': 'Membre', 'ADMIN_ORGANISATION': 'Admin organisation', 'SECRETAIRE': 'Secrétaire', 'TRESORIER': 'Trésorier', 'CONSULTANT': 'Consultant', 'GESTIONNAIRE_RH': 'Gestionnaire RH', 'SUPER_ADMINISTRATEUR': 'Super admin', }; return labels[code] ?? code; } Future _loadOrganisations() async { if (_organisationsLoaded) return; try { final repo = sl(); final list = await repo.getOrganizations(page: 0, size: 200); if (mounted) { setState(() { _organisations = list; _organisationsLoaded = true; }); } } catch (_) { if (mounted) setState(() => _organisationsLoaded = true); } } @override void dispose() { _tabController.dispose(); _queryController.dispose(); _nomController.dispose(); _prenomController.dispose(); _emailController.dispose(); _telephoneController.dispose(); _regionController.dispose(); _villeController.dispose(); _professionController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: UFAppBar( title: 'Recherche Avancée', moduleGradient: ModuleColors.membresGradient, elevation: 0, bottom: TabBar( controller: _tabController, indicatorColor: Colors.white, labelColor: Colors.white, unselectedLabelColor: Colors.white70, tabs: const [ Tab(icon: Icon(Icons.search), text: 'Critères'), Tab(icon: Icon(Icons.list), text: 'Résultats'), Tab(icon: Icon(Icons.analytics), text: 'Statistiques'), ], ), ), body: TabBarView( controller: _tabController, children: [ _buildSearchCriteriaTab(), _buildSearchResultsTab(), _buildStatisticsTab(), ], ), floatingActionButton: _buildSearchFab(), ); } /// Onglet des critères de recherche Widget _buildSearchCriteriaTab() { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Recherche rapide _buildQuickSearchSection(), const SizedBox(height: 24), // Critères détaillés _buildDetailedCriteriaSection(), const SizedBox(height: 24), // Filtres avancés _buildAdvancedFiltersSection(), const SizedBox(height: 24), // Boutons d'action _buildActionButtons(), ], ), ); } /// Section de recherche rapide Widget _buildQuickSearchSection() { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.flash_on, color: AppColors.primary), const SizedBox(width: 8), Text( 'Recherche Rapide', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), TextField( controller: _queryController, decoration: const InputDecoration( labelText: 'Rechercher un membre', hintText: 'Nom, prénom ou email...', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), onSubmitted: (_) => _performQuickSearch(), ), const SizedBox(height: 12), Wrap( spacing: 8, children: [ _buildQuickFilterChip('Membres actifs', () { _selectedStatut = 'ACTIF'; _includeInactifs = false; }), _buildQuickFilterChip('Membres bureau', () { _membreBureau = true; _selectedStatut = 'ACTIF'; }), _buildQuickFilterChip('Responsables', () { _responsable = true; _selectedStatut = 'ACTIF'; }), ], ), ], ), ), ); } /// Section des critères détaillés Widget _buildDetailedCriteriaSection() { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.tune, color: AppColors.primary), const SizedBox(width: 8), Text( 'Critères Détaillés', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: _nomController, decoration: const InputDecoration( labelText: 'Nom', border: OutlineInputBorder(), ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _prenomController, decoration: const InputDecoration( labelText: 'Prénom', border: OutlineInputBorder(), ), ), ), ], ), const SizedBox(height: 16), TextField( controller: _emailController, decoration: const InputDecoration( labelText: 'Email', hintText: 'email@domaine.org', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: _telephoneController, decoration: const InputDecoration( labelText: 'Téléphone', border: OutlineInputBorder(), ), ), ), const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( value: _selectedStatut, decoration: const InputDecoration( labelText: 'Statut', border: OutlineInputBorder(), ), items: const [ DropdownMenuItem(value: null, child: Text('Tous')), DropdownMenuItem(value: 'ACTIF', child: Text('Actif')), DropdownMenuItem(value: 'INACTIF', child: Text('Inactif')), DropdownMenuItem(value: 'SUSPENDU', child: Text('Suspendu')), DropdownMenuItem(value: 'RADIE', child: Text('Radié')), ], onChanged: (value) => setState(() => _selectedStatut = value), ), ), ], ), ], ), ), ); } /// Section des filtres avancés Widget _buildAdvancedFiltersSection() { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.filter_alt, color: AppColors.primary), const SizedBox(width: 8), Text( 'Filtres Avancés', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 16), // Tranche d'âge Text('Tranche d\'âge: ${_ageRange.start.round()}-${_ageRange.end.round()} ans'), RangeSlider( values: _ageRange, min: 18, max: 80, divisions: 62, labels: RangeLabels( '${_ageRange.start.round()}', '${_ageRange.end.round()}', ), onChanged: (values) => setState(() => _ageRange = values), ), const SizedBox(height: 16), // Organisations (multi-select) Text( 'Organisations', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), _organisations.isEmpty && !_organisationsLoaded ? const SizedBox( height: 24, child: Center(child: CircularProgressIndicator(strokeWidth: 2)), ) : Wrap( spacing: 8, runSpacing: 8, children: _organisations .where((o) => o.id != null && o.id!.isNotEmpty) .map((org) { final id = org.id!; final selected = _selectedOrganisations.contains(id); return FilterChip( label: Text(org.nomCourt ?? org.nom, overflow: TextOverflow.ellipsis, maxLines: 1), selected: selected, onSelected: (v) { setState(() { if (v) { _selectedOrganisations.add(id); } else { _selectedOrganisations.remove(id); } }); }, ); }).toList(), ), const SizedBox(height: 16), // Rôles (multi-select) Text( 'Rôles', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: _searchRoleCodes.map((code) { final selected = _selectedRoles.contains(code); return FilterChip( label: Text(_roleDisplayName(code)), selected: selected, onSelected: (v) { setState(() { if (v) { _selectedRoles.add(code); } else { _selectedRoles.remove(code); } }); }, ); }).toList(), ), const SizedBox(height: 16), // Options booléennes CheckboxListTile( title: const Text('Inclure les membres inactifs'), value: _includeInactifs, onChanged: (value) => setState(() => _includeInactifs = value ?? false), ), CheckboxListTile( title: const Text('Membres du bureau uniquement'), value: _membreBureau, onChanged: (value) => setState(() => _membreBureau = value ?? false), ), CheckboxListTile( title: const Text('Responsables uniquement'), value: _responsable, onChanged: (value) => setState(() => _responsable = value ?? false), ), ], ), ), ); } /// Boutons d'action Widget _buildActionButtons() { return Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _clearCriteria, icon: const Icon(Icons.clear), label: const Text('Effacer'), ), ), const SizedBox(width: 16), Expanded( flex: 2, child: ElevatedButton.icon( onPressed: _isSearching ? null : _performAdvancedSearch, icon: _isSearching ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.search), label: Text(_isSearching ? 'Recherche...' : 'Rechercher'), ), ), ], ); } /// Onglet des résultats Widget _buildSearchResultsTab() { if (_currentResult == null) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.search, size: 64, color: AppColors.textTertiary), SizedBox(height: 16), Text( 'Aucune recherche effectuée', style: TextStyle(fontSize: 18, color: AppColors.textTertiary), ), SizedBox(height: 8), Text( 'Utilisez l\'onglet Critères pour lancer une recherche', style: TextStyle(color: AppColors.textTertiary), ), ], ), ); } if (_errorMessage != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, size: 64, color: AppColors.error), const SizedBox(height: 16), Text( 'Erreur de recherche', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( _errorMessage!, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.error), ), const SizedBox(height: 16), ElevatedButton( onPressed: () => setState(() => _errorMessage = null), child: const Text('Réessayer'), ), ], ), ); } return MembreSearchResults( result: _currentResult!, onPageChanged: (page) => _performSearch(_currentCriteria, page: page), ); } /// Onglet des statistiques Widget _buildStatisticsTab() { if (_currentResult?.statistics == null) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.analytics, size: 64, color: AppColors.textTertiary), SizedBox(height: 16), Text( 'Aucune statistique disponible', style: TextStyle(fontSize: 18, color: AppColors.textTertiary), ), SizedBox(height: 8), Text( 'Effectuez une recherche pour voir les statistiques', style: TextStyle(color: AppColors.textTertiary), ), ], ), ); } return SearchStatisticsCard(statistics: _currentResult!.statistics!); } /// FAB de recherche Widget _buildSearchFab() { return FloatingActionButton.extended( onPressed: _isSearching ? null : _performAdvancedSearch, icon: _isSearching ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.search), label: Text(_isSearching ? 'Recherche...' : 'Rechercher'), ); } /// Chip de filtre rapide Widget _buildQuickFilterChip(String label, VoidCallback onTap) { return ActionChip( label: Text(label), onPressed: onTap, backgroundColor: AppColors.primary.withOpacity(0.1), labelStyle: const TextStyle(color: AppColors.primary), ); } /// Effectue une recherche rapide void _performQuickSearch() { if (_queryController.text.trim().isEmpty) return; final criteria = MembreSearchCriteria.quickSearch(_queryController.text.trim()); _performSearch(criteria); } /// Effectue une recherche avancée void _performAdvancedSearch() { final criteria = _buildSearchCriteria(); _performSearch(criteria); } /// Construit les critères de recherche à partir des champs MembreSearchCriteria _buildSearchCriteria() { return MembreSearchCriteria( query: _queryController.text.trim().isEmpty ? null : _queryController.text.trim(), nom: _nomController.text.trim().isEmpty ? null : _nomController.text.trim(), prenom: _prenomController.text.trim().isEmpty ? null : _prenomController.text.trim(), email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(), telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(), statut: _selectedStatut, ageMin: _ageRange.start.round(), ageMax: _ageRange.end.round(), region: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(), ville: _villeController.text.trim().isEmpty ? null : _villeController.text.trim(), profession: _professionController.text.trim().isEmpty ? null : _professionController.text.trim(), organisationIds: _selectedOrganisations.isEmpty ? null : _selectedOrganisations, roles: _selectedRoles.isEmpty ? null : _selectedRoles, membreBureau: _membreBureau ? true : null, responsable: _responsable ? true : null, includeInactifs: _includeInactifs, ); } /// Effectue la recherche void _performSearch(MembreSearchCriteria criteria, {int page = 0}) async { if (!criteria.hasAnyCriteria) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Veuillez spécifier au moins un critère de recherche'), backgroundColor: AppColors.warning, ), ); return; } setState(() { _isSearching = true; _errorMessage = null; _currentCriteria = criteria; }); try { final result = await _searchService.searchMembresAdvanced( criteria: criteria, page: page, ); setState(() { _currentResult = result; _isSearching = false; }); // Basculer vers l'onglet des résultats _tabController.animateTo(1); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result.resultDescription), backgroundColor: AppColors.success, ), ); } catch (e) { setState(() { _errorMessage = e.toString(); _isSearching = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur de recherche: $e'), backgroundColor: AppColors.error, ), ); } } /// Efface tous les critères void _clearCriteria() { setState(() { _queryController.clear(); _nomController.clear(); _prenomController.clear(); _emailController.clear(); _telephoneController.clear(); _regionController.clear(); _villeController.clear(); _professionController.clear(); _selectedStatut = null; _selectedRoles.clear(); _selectedOrganisations.clear(); _ageRange = const RangeValues(18, 65); _includeInactifs = false; _membreBureau = false; _responsable = false; _currentResult = null; _errorMessage = null; }); } }