577 lines
18 KiB
Dart
577 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../../../core/models/membre_search_criteria.dart';
|
|
import '../../../../core/models/membre_search_result.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<AdvancedSearchPage> createState() => _AdvancedSearchPageState();
|
|
}
|
|
|
|
class _AdvancedSearchPageState extends State<AdvancedSearchPage>
|
|
with TickerProviderStateMixin {
|
|
late TabController _tabController;
|
|
MembreSearchCriteria _currentCriteria = MembreSearchCriteria.empty;
|
|
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<String> _selectedRoles = [];
|
|
final List<String> _selectedOrganisations = [];
|
|
RangeValues _ageRange = const RangeValues(18, 65);
|
|
DateTimeRange? _adhesionDateRange;
|
|
bool _includeInactifs = false;
|
|
bool _membreBureau = false;
|
|
bool _responsable = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(length: 3, vsync: this);
|
|
}
|
|
|
|
@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: AppBar(
|
|
title: const Text('Recherche Avancée'),
|
|
backgroundColor: Theme.of(context).primaryColor,
|
|
foregroundColor: Colors.white,
|
|
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: [
|
|
Icon(Icons.flash_on, color: Theme.of(context).primaryColor),
|
|
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: [
|
|
Icon(Icons.tune, color: Theme.of(context).primaryColor),
|
|
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: 'exemple@unionflow.com',
|
|
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<String>(
|
|
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: [
|
|
Icon(Icons.filter_alt, color: Theme.of(context).primaryColor),
|
|
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),
|
|
|
|
// 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: Colors.grey),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Aucune recherche effectuée',
|
|
style: TextStyle(fontSize: 18, color: Colors.grey),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Utilisez l\'onglet Critères pour lancer une recherche',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (_errorMessage != null) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error, size: 64, color: Colors.red),
|
|
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: Colors.red),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () => setState(() => _errorMessage = null),
|
|
child: const Text('Réessayer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return MembreSearchResults(result: _currentResult!);
|
|
}
|
|
|
|
/// Onglet des statistiques
|
|
Widget _buildStatisticsTab() {
|
|
if (_currentResult?.statistics == null) {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.analytics, size: 64, color: Colors.grey),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Aucune statistique disponible',
|
|
style: TextStyle(fontSize: 18, color: Colors.grey),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Effectuez une recherche pour voir les statistiques',
|
|
style: TextStyle(color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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: Theme.of(context).primaryColor.withOpacity(0.1),
|
|
labelStyle: TextStyle(color: Theme.of(context).primaryColor),
|
|
);
|
|
}
|
|
|
|
/// 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) async {
|
|
if (!criteria.hasAnyCriteria) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez spécifier au moins un critère de recherche'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isSearching = true;
|
|
_errorMessage = null;
|
|
_currentCriteria = criteria;
|
|
});
|
|
|
|
try {
|
|
// TODO: Appeler le service de recherche
|
|
// final result = await _searchService.searchMembresAdvanced(criteria: criteria);
|
|
|
|
// Simulation pour l'instant
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
final result = MembreSearchResult.empty(criteria);
|
|
|
|
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: Colors.green,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = e.toString();
|
|
_isSearching = false;
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erreur de recherche: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
_adhesionDateRange = null;
|
|
_includeInactifs = false;
|
|
_membreBureau = false;
|
|
_responsable = false;
|
|
_currentResult = null;
|
|
_errorMessage = null;
|
|
});
|
|
}
|
|
}
|