import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/data/models/sector_model.dart'; import 'package:geosector_app/core/data/models/membre_model.dart'; import 'package:geosector_app/core/repositories/passage_repository.dart'; import 'package:geosector_app/core/repositories/sector_repository.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/repositories/membre_repository.dart'; import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart'; import 'dart:math' as math; /// Class pour dessiner les petits points blancs sur le fond class DotsPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.5) ..style = PaintingStyle.fill; final random = math.Random(42); // Seed fixe pour consistance final numberOfDots = (size.width * size.height) ~/ 1500; for (int i = 0; i < numberOfDots; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = 1.0 + random.nextDouble() * 2.0; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class AdminHistoryPage extends StatefulWidget { const AdminHistoryPage({super.key}); @override State createState() => _AdminHistoryPageState(); } class _AdminHistoryPageState extends State { // État des filtres String searchQuery = ''; String selectedSector = 'Tous'; String selectedUser = 'Tous'; String selectedType = 'Tous'; String selectedPaymentMethod = 'Tous'; String selectedPeriod = 'Tous'; // Période par défaut DateTimeRange? selectedDateRange; // Contrôleur pour la recherche final TextEditingController _searchController = TextEditingController(); // IDs pour les filtres int? selectedSectorId; int? selectedUserId; // Listes pour les filtres List _sectors = []; List _membres = []; // Repositories late PassageRepository _passageRepository; late SectorRepository _sectorRepository; late UserRepository _userRepository; late MembreRepository _membreRepository; // Passages formatés pour l'affichage List> _formattedPassages = []; // Passages originaux pour l'édition List _originalPassages = []; // État de chargement bool _isLoading = true; String _errorMessage = ''; @override void initState() { super.initState(); // Initialiser les filtres _initializeFilters(); // Charger les filtres présélectionnés depuis Hive si disponibles _loadPreselectedFilters(); } @override void didChangeDependencies() { super.didChangeDependencies(); // Récupérer les repositories une seule fois _loadRepositories(); } // Charger les repositories et les données void _loadRepositories() { try { // Utiliser les instances globales définies dans app.dart _passageRepository = passageRepository; _userRepository = userRepository; _sectorRepository = sectorRepository; _membreRepository = membreRepository; // Charger les secteurs et les membres _loadSectorsAndMembres(); // Charger les passages _loadPassages(); } catch (e) { setState(() { _isLoading = false; _errorMessage = 'Erreur lors du chargement des repositories: $e'; }); } } // Charger les secteurs et les membres void _loadSectorsAndMembres() { try { // Récupérer la liste des secteurs _sectors = _sectorRepository.getAllSectors(); debugPrint('Nombre de secteurs récupérés: ${_sectors.length}'); // Récupérer la liste des membres _membres = _membreRepository.getAllMembres(); debugPrint('Nombre de membres récupérés: ${_membres.length}'); } catch (e) { debugPrint('Erreur lors du chargement des secteurs et membres: $e'); } } // Charger les passages void _loadPassages() { setState(() { _isLoading = true; }); try { // Récupérer les passages final List allPassages = _passageRepository.getAllPassages(); // Stocker les passages originaux pour l'édition _originalPassages = allPassages; // Convertir les passages en format attendu par PassagesListWidget _formattedPassages = _formatPassagesForWidget( allPassages, _sectorRepository, _membreRepository); setState(() { _isLoading = false; }); } catch (e) { setState(() { _isLoading = false; _errorMessage = 'Erreur lors du chargement des passages: $e'; }); } } // Initialiser les filtres void _initializeFilters() { // Par défaut, on n'applique pas de filtre par utilisateur ou secteur selectedSectorId = null; selectedUserId = null; // Période par défaut : toutes les périodes selectedPeriod = 'Tous'; // Plage de dates par défaut : aucune restriction selectedDateRange = null; } // Charger les filtres présélectionnés depuis Hive void _loadPreselectedFilters() { try { // Utiliser Hive directement sans async if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); // Charger le secteur présélectionné final int? preselectedSectorId = settingsBox.get('history_selectedSectorId'); final String? preselectedSectorName = settingsBox.get('history_selectedSectorName'); final int? preselectedTypeId = settingsBox.get('history_selectedTypeId'); if (preselectedSectorId != null && preselectedSectorName != null) { selectedSectorId = preselectedSectorId; selectedSector = preselectedSectorName; debugPrint('Secteur présélectionné: $preselectedSectorName (ID: $preselectedSectorId)'); } if (preselectedTypeId != null) { selectedType = preselectedTypeId.toString(); debugPrint('Type de passage présélectionné: $preselectedTypeId'); } // Nettoyer les valeurs après utilisation pour ne pas les réutiliser la prochaine fois settingsBox.delete('history_selectedSectorId'); settingsBox.delete('history_selectedSectorName'); settingsBox.delete('history_selectedTypeId'); } } catch (e) { debugPrint('Erreur lors du chargement des filtres présélectionnés: $e'); } } @override void dispose() { _searchController.dispose(); super.dispose(); } // Méthode pour appliquer tous les filtres List> _getFilteredPassages() { try { var filtered = _formattedPassages.where((passage) { try { // Ne plus exclure automatiquement les passages de type 2 // car on propose maintenant un filtre par type dans les "Filtres avancés" // Filtrer par utilisateur if (selectedUserId != null && passage.containsKey('fkUser') && passage['fkUser'] != selectedUserId) { return false; } // Filtrer par secteur if (selectedSectorId != null && passage.containsKey('fkSector') && passage['fkSector'] != selectedSectorId) { return false; } // Filtrer par type de passage if (selectedType != 'Tous') { try { final int? selectedTypeId = int.tryParse(selectedType); if (selectedTypeId != null) { if (!passage.containsKey('type') || passage['type'] != selectedTypeId) { return false; } } } catch (e) { debugPrint('Erreur de filtrage par type: $e'); } } // Filtrer par mode de règlement if (selectedPaymentMethod != 'Tous') { try { final int? selectedPaymentId = int.tryParse(selectedPaymentMethod); if (selectedPaymentId != null) { if (!passage.containsKey('payment') || passage['payment'] != selectedPaymentId) { return false; } } } catch (e) { debugPrint('Erreur de filtrage par mode de règlement: $e'); } } // Filtrer par recherche if (searchQuery.isNotEmpty) { try { final query = searchQuery.toLowerCase(); final address = passage.containsKey('address') ? passage['address']?.toString().toLowerCase() ?? '' : ''; final name = passage.containsKey('name') ? passage['name']?.toString().toLowerCase() ?? '' : ''; final notes = passage.containsKey('notes') ? passage['notes']?.toString().toLowerCase() ?? '' : ''; if (!address.contains(query) && !name.contains(query) && !notes.contains(query)) { return false; } } catch (e) { debugPrint('Erreur de filtrage par recherche: $e'); return false; } } // Filtrer par période/date if (selectedDateRange != null) { try { if (passage.containsKey('date') && passage['date'] is DateTime) { final DateTime passageDate = passage['date'] as DateTime; if (passageDate.isBefore(selectedDateRange!.start) || passageDate.isAfter(selectedDateRange!.end)) { return false; } } } catch (e) { debugPrint('Erreur de filtrage par date: $e'); } } return true; } catch (e) { debugPrint('Erreur lors du filtrage d\'un passage: $e'); return false; } }).toList(); // Trier par date décroissante (plus récent en premier) filtered.sort((a, b) { try { final DateTime dateA = a['date'] as DateTime; final DateTime dateB = b['date'] as DateTime; return dateB.compareTo(dateA); } catch (e) { return 0; } }); debugPrint( 'Passages filtrés: ${filtered.length}/${_formattedPassages.length}'); return filtered; } catch (e) { debugPrint('Erreur globale lors du filtrage: $e'); return _formattedPassages; } } // Mettre à jour le filtre par secteur void _updateSectorFilter(String sectorName, int? sectorId) { setState(() { selectedSector = sectorName; selectedSectorId = sectorId; }); } // Mettre à jour le filtre par utilisateur void _updateUserFilter(String userName, int? userId) { setState(() { selectedUser = userName; selectedUserId = userId; }); } // Mettre à jour le filtre par période void _updatePeriodFilter(String period) { setState(() { selectedPeriod = period; // Mettre à jour la plage de dates en fonction de la période final DateTime now = DateTime.now(); switch (period) { case 'Derniers 15 jours': selectedDateRange = DateTimeRange( start: now.subtract(const Duration(days: 15)), end: now, ); break; case 'Dernière semaine': selectedDateRange = DateTimeRange( start: now.subtract(const Duration(days: 7)), end: now, ); break; case 'Dernier mois': selectedDateRange = DateTimeRange( start: DateTime(now.year, now.month - 1, now.day), end: now, ); break; case 'Tous': selectedDateRange = null; break; } }); } @override Widget build(BuildContext context) { // Afficher un widget de chargement ou d'erreur si nécessaire if (_isLoading) { return Stack( children: [ // Fond dégradé avec petits points blancs Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.red.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox( width: double.infinity, height: double.infinity), ), ), const Center( child: CircularProgressIndicator(), ), ], ); } if (_errorMessage.isNotEmpty) { return _buildErrorWidget(_errorMessage); } // Retourner le widget principal avec les données chargées return Stack( children: [ // Fond dégradé avec petits points blancs Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.red.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox(width: double.infinity, height: double.infinity), ), ), // Contenu de la page LayoutBuilder( builder: (context, constraints) { final passages = _getFilteredPassages(); return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: ConstrainedBox( constraints: BoxConstraints( minHeight: constraints.maxHeight - 32, // Moins le padding ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre de la page Text( 'Historique des passages', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 16), // Filtres supplémentaires (secteur, utilisateur, période) _buildAdditionalFilters(context), const SizedBox(height: 16), // Widget de liste des passages avec hauteur fixe SizedBox( height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible child: PassagesListWidget( passages: passages, showFilters: false, // Désactivé car les filtres sont maintenant dans la card "Filtres avancés" showSearch: false, // Désactivé car la recherche est maintenant dans la card "Filtres avancés" showActions: true, // Ne plus passer les filtres individuels car ils sont maintenant appliqués dans _getFilteredPassages() onPassageSelected: (passage) { _openPassageEditDialog(context, passage); }, onReceiptView: (passage) { _showReceiptDialog(context, passage); }, onDetailsView: (passage) { _showDetailsDialog(context, passage); }, onPassageEdit: (passage) { // Action pour modifier le passage // Cette fonctionnalité pourrait être implémentée ultérieurement }, ), ), ], ), ), ); }, ), ], ); } // Widget d'erreur pour afficher un message d'erreur Widget _buildErrorWidget(String message) { return Stack( children: [ // Fond dégradé avec petits points blancs Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.red.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox(width: double.infinity, height: double.infinity), ), ), Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.error_outline, color: Colors.red, size: 64, ), const SizedBox(height: 16), Text( 'Erreur', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.red[700], ), ), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 24), ElevatedButton( onPressed: () { // Recharger la page setState(() {}); }, child: const Text('Réessayer'), ), ], ), ), ), ], ); } // Convertir les passages du modèle Hive vers le format attendu par le widget List> _formatPassagesForWidget( List passages, SectorRepository sectorRepository, MembreRepository membreRepository) { return passages.map((passage) { // Récupérer le secteur associé au passage (si fkSector n'est pas null) final SectorModel? sector = passage.fkSector != null ? sectorRepository.getSectorById(passage.fkSector!) : null; // Récupérer le membre associé au passage final MembreModel? membre = membreRepository.getMembreById(passage.fkUser); // Construire l'adresse complète final String address = '${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}'; // Déterminer si le passage a une erreur d'envoi de reçu final bool hasError = passage.emailErreur.isNotEmpty; return { 'id': passage.id, if (passage.passedAt != null) 'date': passage.passedAt!, 'address': address, // Adresse complète pour l'affichage // Champs séparés pour l'édition 'numero': passage.numero, 'rueBis': passage.rueBis, 'rue': passage.rue, 'ville': passage.ville, 'residence': passage.residence, 'appt': passage.appt, 'niveau': passage.niveau, 'fkHabitat': passage.fkHabitat, 'fkSector': passage.fkSector, 'sector': sector?.libelle ?? 'Secteur inconnu', 'fkUser': passage.fkUser, 'user': membre?.name ?? 'Membre inconnu', 'type': passage.fkType, 'amount': double.tryParse(passage.montant) ?? 0.0, 'payment': passage.fkTypeReglement, 'email': passage.email, 'hasReceipt': passage.nomRecu.isNotEmpty, 'hasError': hasError, 'notes': passage.remarque, 'name': passage.name, 'phone': passage.phone, 'montant': passage.montant, 'remarque': passage.remarque, // Autres champs utiles 'fkOperation': passage.fkOperation, 'passedAt': passage.passedAt, 'lastSyncedAt': passage.lastSyncedAt, 'isActive': passage.isActive, 'isSynced': passage.isSynced, }; }).toList(); } void _showReceiptDialog(BuildContext context, Map passage) { final int passageId = passage['id'] as int; showDialog( context: context, builder: (context) => AlertDialog( title: Text('Reçu du passage #$passageId'), content: const SizedBox( width: 500, height: 600, child: Center( child: Text('Aperçu du reçu PDF'), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { // Action pour télécharger le reçu Navigator.pop(context); }, child: const Text('Télécharger'), ), ], ), ); } void _showDetailsDialog(BuildContext context, Map passage) { final int passageId = passage['id'] as int; final DateTime date = passage['date'] as DateTime; showDialog( context: context, builder: (context) => AlertDialog( title: Text('Détails du passage #$passageId'), content: SizedBox( width: 500, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildDetailRow('Date', '${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}'), _buildDetailRow('Adresse', passage['address'] as String), _buildDetailRow('Secteur', passage['sector'] as String), _buildDetailRow('Collecteur', passage['user'] as String), _buildDetailRow( 'Type', AppKeys.typesPassages[passage['type']]?['titre'] ?? 'Inconnu'), _buildDetailRow('Montant', '${passage['amount']} €'), _buildDetailRow( 'Mode de paiement', AppKeys.typesReglements[passage['payment']]?['titre'] ?? 'Inconnu'), _buildDetailRow('Email', passage['email'] as String), _buildDetailRow( 'Reçu envoyé', passage['hasReceipt'] ? 'Oui' : 'Non'), _buildDetailRow( 'Erreur d\'envoi', passage['hasError'] ? 'Oui' : 'Non'), _buildDetailRow( 'Notes', (passage['notes'] as String).isEmpty ? '-' : passage['notes'] as String), const SizedBox(height: 16), const Text( 'Historique des actions', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHistoryItem( date, passage['user'] as String, 'Création du passage', ), if (passage['hasReceipt']) _buildHistoryItem( date.add(const Duration(minutes: 5)), 'Système', 'Envoi du reçu par email', ), if (passage['hasError']) _buildHistoryItem( date.add(const Duration(minutes: 6)), 'Système', 'Erreur lors de l\'envoi du reçu', ), ], ), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Fermer'), ), ElevatedButton( onPressed: () { // Action pour modifier le passage Navigator.pop(context); }, child: const Text('Modifier'), ), ], ), ); } void _openPassageEditDialog( BuildContext context, Map passage) async { try { debugPrint('=== DEBUT _openPassageEditDialog ==='); // Récupérer l'ID du passage final int passageId = passage['id'] as int; debugPrint('Recherche du passage ID: $passageId'); // Trouver le PassageModel original dans la liste final PassageModel? passageModel = _originalPassages.where((p) => p.id == passageId).firstOrNull; if (passageModel == null) { throw Exception('Passage original introuvable avec l\'ID: $passageId'); } debugPrint('PassageModel original trouvé'); if (!mounted) { debugPrint('Widget non monté, abandon'); return; } debugPrint('Ouverture du dialog...'); showDialog( context: context, barrierDismissible: false, builder: (context) => PassageFormDialog( passage: passageModel, title: 'Modifier le passage', passageRepository: _passageRepository, userRepository: _userRepository, operationRepository: operationRepository, onSuccess: () { debugPrint('Dialog fermé avec succès'); // Recharger les données après modification _loadPassages(); }, ), ); debugPrint('=== FIN _openPassageEditDialog ==='); } catch (e, stackTrace) { debugPrint('=== ERREUR _openPassageEditDialog ==='); debugPrint('Erreur: $e'); debugPrint('StackTrace: $stackTrace'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors de l\'ouverture du formulaire: $e'), backgroundColor: Colors.red, ), ); } } } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 150, child: Text( '$label :', style: const TextStyle(fontWeight: FontWeight.bold), ), ), Expanded( child: Text(value), ), ], ), ); } Widget _buildHistoryItem(DateTime date, String user, String action) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), ), Text('$user - $action'), const Divider(), ], ), ); } // Construction des filtres supplémentaires Widget _buildAdditionalFilters(BuildContext context) { final theme = Theme.of(context); final size = MediaQuery.of(context).size; final isDesktop = size.width > 900; return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), color: Colors.white, // Fond opaque child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Filtres avancés', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), const SizedBox(height: 16), // Champ de recherche _buildSearchField(theme), const SizedBox(height: 16), // Disposition des filtres en fonction de la taille de l'écran isDesktop ? Column( children: [ // Première ligne : Secteur, Utilisateur, Période Row( children: [ // Filtre par secteur Expanded( child: _buildSectorFilter(theme, _sectors), ), const SizedBox(width: 16), // Filtre par membre Expanded( child: _buildMembreFilter(theme, _membres), ), const SizedBox(width: 16), // Filtre par période Expanded( child: _buildPeriodFilter(theme), ), ], ), const SizedBox(height: 16), // Deuxième ligne : Type de passage, Mode de règlement Row( children: [ // Filtre par type de passage Expanded( child: _buildTypeFilter(theme), ), const SizedBox(width: 16), // Filtre par mode de règlement Expanded( child: _buildPaymentFilter(theme), ), // Espacement pour équilibrer avec la ligne du dessus (3 colonnes) const Expanded(child: SizedBox()), ], ), ], ) : Column( children: [ // Filtre par secteur _buildSectorFilter(theme, _sectors), const SizedBox(height: 16), // Filtre par membre _buildMembreFilter(theme, _membres), const SizedBox(height: 16), // Filtre par période _buildPeriodFilter(theme), const SizedBox(height: 16), // Filtre par type de passage _buildTypeFilter(theme), const SizedBox(height: 16), // Filtre par mode de règlement _buildPaymentFilter(theme), ], ), ], ), ), ); } // Construction du filtre par secteur Widget _buildSectorFilter(ThemeData theme, List sectors) { // Vérifier si la liste des secteurs est vide ou si selectedSector n'est pas dans la liste bool isSelectedSectorValid = selectedSector == 'Tous' || sectors.any((s) => s.libelle == selectedSector); // Si selectedSector n'est pas valide, le réinitialiser à 'Tous' if (!isSelectedSectorValid) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { selectedSector = 'Tous'; selectedSectorId = null; }); } }); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Secteur', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12.0), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8.0), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: isSelectedSectorValid ? selectedSector : 'Tous', isExpanded: true, icon: const Icon(Icons.arrow_drop_down), items: [ const DropdownMenuItem( value: 'Tous', child: Text('Tous les secteurs'), ), ...sectors.map((sector) { final String libelle = sector.libelle.isNotEmpty ? sector.libelle : 'Secteur ${sector.id}'; return DropdownMenuItem( value: libelle, child: Text( libelle, overflow: TextOverflow.ellipsis, ), ); }), ], onChanged: (String? value) { if (value != null) { if (value == 'Tous') { _updateSectorFilter('Tous', null); } else { try { // Trouver le secteur correspondant final sector = sectors.firstWhere( (s) => s.libelle == value, orElse: () => sectors.isNotEmpty ? sectors.first : throw Exception('Liste de secteurs vide'), ); // Convertir sector.id en int? si nécessaire _updateSectorFilter(value, sector.id); } catch (e) { debugPrint('Erreur lors de la sélection du secteur: $e'); _updateSectorFilter('Tous', null); } } } }, ), ), ), ], ); } // Construction du filtre par membre Widget _buildMembreFilter(ThemeData theme, List membres) { // Fonction pour formater le nom d'affichage d'un membre String formatMembreDisplayName(MembreModel membre) { final String firstName = membre.firstName ?? ''; final String name = membre.name ?? ''; final String sectName = membre.sectName ?? ''; // Construire le nom de base String displayName = ''; if (firstName.isNotEmpty && name.isNotEmpty) { displayName = '$firstName $name'; } else if (name.isNotEmpty) { displayName = name; } else if (firstName.isNotEmpty) { displayName = firstName; } else { displayName = 'Membre inconnu'; } // Ajouter le sectName entre parenthèses s'il existe if (sectName.isNotEmpty) { displayName = '$displayName ($sectName)'; } return displayName; } // Trier les membres par nom de famille final List sortedMembres = [...membres]; sortedMembres.sort((a, b) { final String nameA = a.name ?? ''; final String nameB = b.name ?? ''; return nameA.compareTo(nameB); }); // Créer une map pour retrouver les membres par leur nom d'affichage final Map membreDisplayMap = {}; for (final membre in sortedMembres) { final displayName = formatMembreDisplayName(membre); membreDisplayMap[displayName] = membre; } // Vérifier si la liste des membres est vide ou si selectedUser n'est pas dans la liste bool isSelectedUserValid = selectedUser == 'Tous' || membreDisplayMap.containsKey(selectedUser); // Si selectedUser n'est pas valide, le réinitialiser à 'Tous' if (!isSelectedUserValid) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { selectedUser = 'Tous'; selectedUserId = null; }); } }); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Membre', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12.0), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8.0), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: isSelectedUserValid ? selectedUser : 'Tous', isExpanded: true, icon: const Icon(Icons.arrow_drop_down), items: [ const DropdownMenuItem( value: 'Tous', child: Text('Tous les membres'), ), ...membreDisplayMap.entries.map((entry) { final String displayName = entry.key; return DropdownMenuItem( value: displayName, child: Text( displayName, overflow: TextOverflow.ellipsis, ), ); }), ], onChanged: (String? value) { if (value != null) { if (value == 'Tous') { _updateUserFilter('Tous', null); } else { try { // Trouver le membre correspondant dans la map final membre = membreDisplayMap[value]; if (membre != null) { final int membreId = membre.id; _updateUserFilter(value, membreId); } else { throw Exception('Membre non trouvé: $value'); } } catch (e) { debugPrint('Erreur lors de la sélection du membre: $e'); _updateUserFilter('Tous', null); } } } }, ), ), ), ], ); } // Construction du filtre par période Widget _buildPeriodFilter(ThemeData theme) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Période', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12.0), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8.0), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: selectedPeriod, isExpanded: true, icon: const Icon(Icons.arrow_drop_down), items: const [ DropdownMenuItem( value: 'Tous', child: Text('Toutes les périodes'), ), DropdownMenuItem( value: 'Derniers 15 jours', child: Text('Derniers 15 jours'), ), DropdownMenuItem( value: 'Dernière semaine', child: Text('Dernière semaine'), ), DropdownMenuItem( value: 'Dernier mois', child: Text('Dernier mois'), ), ], onChanged: (String? value) { if (value != null) { _updatePeriodFilter(value); } }, ), ), ), // Afficher la plage de dates sélectionnée si elle existe if (selectedDateRange != null && selectedPeriod != 'Tous') Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Icon( Icons.date_range, size: 16, color: theme.colorScheme.primary, ), const SizedBox(width: 8), Text( 'Du ${selectedDateRange!.start.day}/${selectedDateRange!.start.month}/${selectedDateRange!.start.year} au ${selectedDateRange!.end.day}/${selectedDateRange!.end.month}/${selectedDateRange!.end.year}', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ), ], ), ), ], ); } // Construction du champ de recherche Widget _buildSearchField(ThemeData theme) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Recherche', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher par adresse ou nom...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { _searchController.clear(); searchQuery = ''; }); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8.0), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), onChanged: (value) { setState(() { searchQuery = value; }); }, ), ], ); } // Construction du filtre par type de passage Widget _buildTypeFilter(ThemeData theme) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Type de passage', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12.0), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8.0), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: selectedType, isExpanded: true, icon: const Icon(Icons.arrow_drop_down), items: [ const DropdownMenuItem( value: 'Tous', child: Text('Tous les types'), ), ...AppKeys.typesPassages.entries.map((entry) { return DropdownMenuItem( value: entry.key.toString(), child: Text( entry.value['titre'] as String, overflow: TextOverflow.ellipsis, ), ); }), ], onChanged: (String? value) { if (value != null) { setState(() { selectedType = value; }); } }, ), ), ), ], ); } // Construction du filtre par mode de règlement Widget _buildPaymentFilter(ThemeData theme) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Mode de règlement', style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12.0), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline), borderRadius: BorderRadius.circular(8.0), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: selectedPaymentMethod, isExpanded: true, icon: const Icon(Icons.arrow_drop_down), items: [ const DropdownMenuItem( value: 'Tous', child: Text('Tous les modes'), ), ...AppKeys.typesReglements.entries.map((entry) { return DropdownMenuItem( value: entry.key.toString(), child: Text( entry.value['titre'] as String, overflow: TextOverflow.ellipsis, ), ); }), ], onChanged: (String? value) { if (value != null) { setState(() { selectedPaymentMethod = value; }); } }, ), ), ), ], ); } }