import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/theme/app_theme.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.withValues(alpha: 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; } // Enum pour gérer les types de tri enum PassageSortType { dateDesc, // Plus récent en premier (défaut) dateAsc, // Plus ancien en premier addressAsc, // Adresse A-Z addressDesc, // Adresse Z-A } 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; // État du tri actuel PassageSortType _currentSort = PassageSortType.dateDesc; // 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 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; 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(); } // Nouvelle méthode pour filtrer une liste de passages déjà formatés List> _getFilteredPassagesFromList( List> passages) { try { var filtered = passages.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(); // Appliquer le tri sélectionné filtered = _sortPassages(filtered); debugPrint('Passages filtrés: ${filtered.length}/${passages.length}'); return filtered; } catch (e) { debugPrint('Erreur globale lors du filtrage: $e'); return passages; } } // Méthode pour trier les passages selon le type de tri sélectionné List> _sortPassages( List> passages) { final sortedPassages = List>.from(passages); switch (_currentSort) { case PassageSortType.dateDesc: sortedPassages.sort((a, b) { try { return (b['date'] as DateTime).compareTo(a['date'] as DateTime); } catch (e) { return 0; } }); break; case PassageSortType.dateAsc: sortedPassages.sort((a, b) { try { return (a['date'] as DateTime).compareTo(b['date'] as DateTime); } catch (e) { return 0; } }); break; case PassageSortType.addressAsc: sortedPassages.sort((a, b) { try { // Tri intelligent par rue, numéro (numérique), rueBis final String rueA = a['rue'] ?? ''; final String rueB = b['rue'] ?? ''; final String numeroA = a['numero'] ?? ''; final String numeroB = b['numero'] ?? ''; final String rueBisA = a['rueBis'] ?? ''; final String rueBisB = b['rueBis'] ?? ''; // D'abord comparer les rues int rueCompare = rueA.toLowerCase().compareTo(rueB.toLowerCase()); if (rueCompare != 0) return rueCompare; // Si les rues sont identiques, comparer les numéros (numériquement) int numA = int.tryParse(numeroA) ?? 0; int numB = int.tryParse(numeroB) ?? 0; int numCompare = numA.compareTo(numB); if (numCompare != 0) return numCompare; // Si les numéros sont identiques, comparer les rueBis return rueBisA.toLowerCase().compareTo(rueBisB.toLowerCase()); } catch (e) { return 0; } }); break; case PassageSortType.addressDesc: sortedPassages.sort((a, b) { try { // Tri intelligent inversé par rue, numéro (numérique), rueBis final String rueA = a['rue'] ?? ''; final String rueB = b['rue'] ?? ''; final String numeroA = a['numero'] ?? ''; final String numeroB = b['numero'] ?? ''; final String rueBisA = a['rueBis'] ?? ''; final String rueBisB = b['rueBis'] ?? ''; // D'abord comparer les rues (inversé) int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase()); if (rueCompare != 0) return rueCompare; // Si les rues sont identiques, comparer les numéros (inversé numériquement) int numA = int.tryParse(numeroA) ?? 0; int numB = int.tryParse(numeroB) ?? 0; int numCompare = numB.compareTo(numA); if (numCompare != 0) return numCompare; // Si les numéros sont identiques, comparer les rueBis (inversé) return rueBisB.toLowerCase().compareTo(rueBisA.toLowerCase()); } catch (e) { return 0; } }); break; } return sortedPassages; } // 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) { 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: [ // Filtres supplémentaires (secteur, utilisateur, période) _buildAdditionalFilters(context), const SizedBox(height: 16), // Widget de liste des passages avec hauteur fixe et ValueListenableBuilder SizedBox( height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible child: ValueListenableBuilder( valueListenable: Hive.box(AppKeys.passagesBoxName) .listenable(), builder: (context, Box passagesBox, child) { // Reconvertir les passages à chaque changement final List allPassages = passagesBox.values.toList(); // Convertir et formater les passages final formattedPassages = _formatPassagesForWidget( allPassages, _sectorRepository, _membreRepository); // Appliquer les filtres final filteredPassages = _getFilteredPassagesFromList(formattedPassages); return PassagesListWidget( showAddButton: true, // Activer le bouton de création onAddPassage: () async { // Ouvrir le dialogue de création de passage await showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return PassageFormDialog( title: 'Nouveau passage', passageRepository: _passageRepository, userRepository: _userRepository, operationRepository: operationRepository, onSuccess: () { // Le widget se rafraîchira automatiquement via ValueListenableBuilder }, ); }, ); }, sortingButtons: Row( children: [ // Bouton tri par date avec icône calendrier IconButton( icon: Icon( Icons.calendar_today, size: 20, color: _currentSort == PassageSortType.dateDesc || _currentSort == PassageSortType.dateAsc ? Theme.of(context).colorScheme.primary : Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6), ), tooltip: _currentSort == PassageSortType.dateAsc ? 'Tri par date (ancien en premier)' : 'Tri par date (récent en premier)', onPressed: () { setState(() { if (_currentSort == PassageSortType.dateDesc) { _currentSort = PassageSortType.dateAsc; } else { _currentSort = PassageSortType.dateDesc; } }); }, ), // Indicateur de direction pour la date if (_currentSort == PassageSortType.dateDesc || _currentSort == PassageSortType.dateAsc) Icon( _currentSort == PassageSortType.dateAsc ? Icons.arrow_upward : Icons.arrow_downward, size: 14, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 4), // Bouton tri par adresse avec icône maison IconButton( icon: Icon( Icons.home, size: 20, color: _currentSort == PassageSortType.addressDesc || _currentSort == PassageSortType.addressAsc ? Theme.of(context).colorScheme.primary : Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6), ), tooltip: _currentSort == PassageSortType.addressAsc ? 'Tri par adresse (A-Z)' : 'Tri par adresse (Z-A)', onPressed: () { setState(() { if (_currentSort == PassageSortType.addressAsc) { _currentSort = PassageSortType.addressDesc; } else { _currentSort = PassageSortType.addressAsc; } }); }, ), // Indicateur de direction pour l'adresse if (_currentSort == PassageSortType.addressDesc || _currentSort == PassageSortType.addressAsc) Icon( _currentSort == PassageSortType.addressAsc ? Icons.arrow_upward : Icons.arrow_downward, size: 14, color: Theme.of(context).colorScheme.primary, ), ], ), passages: filteredPassages, showFilters: false, showSearch: false, showActions: true, // Le widget gère maintenant le flux conditionnel par défaut onPassageSelected: null, onReceiptView: (passage) { _showReceiptDialog(context, passage); }, onDetailsView: (passage) { _showDetailsDialog(context, passage); }, onPassageEdit: (passage) { // Action pour modifier le passage }, onPassageDelete: (passage) { _showDeleteConfirmationDialog(passage); }, ); }, ), ), ], ), ), ); }, ), ], ); } // 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: AppTheme.r(context, 24), fontWeight: FontWeight.bold, color: Colors.red[700], ), ), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: TextStyle(fontSize: AppTheme.r(context, 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; // Récupérer l'ID de l'utilisateur courant pour déterminer la propriété final currentUserId = _userRepository.getCurrentUser()?.id; 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, 'isOwnedByCurrentUser': passage.fkUser == currentUserId, // Ajout du champ pour le widget }; }).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'), ), ], ), ); } // Méthode pour conserver l'ancienne _showDetailsDialog pour les autres usages 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'), ), ], ), ); } // Méthode extraite pour ouvrir le dialog de modification 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: TextStyle( fontWeight: FontWeight.bold, fontSize: AppTheme.r(context, 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: [ // 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 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), hint: const Text('Sélectionner un secteur'), 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 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), hint: const Text('Sélectionner un membre'), 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: [ 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), hint: const Text('Sélectionner une période'), 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 TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher par adresse, nom, secteur ou membre...', 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 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), hint: const Text('Sélectionner un type de passage'), 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; }); } }, ), ), ); } // Afficher le dialog de confirmation de suppression void _showDeleteConfirmationDialog(Map passage) { final TextEditingController confirmController = TextEditingController(); // Récupérer l'ID du passage et trouver le PassageModel original final int passageId = passage['id'] as int; final PassageModel? passageModel = _originalPassages.where((p) => p.id == passageId).firstOrNull; if (passageModel == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Impossible de trouver le passage'), backgroundColor: Colors.red, ), ); return; } final String streetNumber = passageModel.numero; final String fullAddress = '${passageModel.numero} ${passageModel.rueBis} ${passageModel.rue}' .trim(); showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) { return AlertDialog( title: const Row( children: [ Icon(Icons.warning, color: Colors.red, size: 28), SizedBox(width: 8), Text('Confirmation de suppression'), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'ATTENTION : Cette action est irréversible !', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.red, fontSize: AppTheme.r(context, 16), ), ), const SizedBox(height: 16), Text( 'Vous êtes sur le point de supprimer définitivement le passage :', style: TextStyle(color: Colors.grey[800]), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress, style: TextStyle( fontWeight: FontWeight.w600, fontSize: AppTheme.r(context, 14), ), ), const SizedBox(height: 4), if (passage['user'] != null) Text( 'Collecteur: ${passage['user']}', style: TextStyle( fontSize: AppTheme.r(context, 12), color: Colors.grey[600], ), ), if (passage['date'] != null) Text( 'Date: ${_formatDate(passage['date'] as DateTime)}', style: TextStyle( fontSize: AppTheme.r(context, 12), color: Colors.grey[600], ), ), ], ), ), const SizedBox(height: 20), const Text( 'Pour confirmer la suppression, veuillez saisir le numéro de rue de ce passage :', style: TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 12), TextField( controller: confirmController, decoration: InputDecoration( labelText: 'Numéro de rue', hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.home), ), keyboardType: TextInputType.text, textCapitalization: TextCapitalization.characters, ), ], ), ), actions: [ TextButton( onPressed: () { confirmController.dispose(); Navigator.of(dialogContext).pop(); }, child: const Text('Annuler'), ), ElevatedButton( onPressed: () async { // Vérifier que le numéro saisi correspond final enteredNumber = confirmController.text.trim(); if (enteredNumber.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Veuillez saisir le numéro de rue'), backgroundColor: Colors.orange, ), ); return; } if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Le numéro de rue ne correspond pas'), backgroundColor: Colors.red, ), ); return; } // Fermer le dialog confirmController.dispose(); Navigator.of(dialogContext).pop(); // Effectuer la suppression await _deletePassage(passageModel); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Supprimer définitivement'), ), ], ); }, ); } // Supprimer un passage Future _deletePassage(PassageModel passage) async { try { // Appeler le repository pour supprimer via l'API final success = await _passageRepository.deletePassageViaApi(passage.id); if (success && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Passage supprimé avec succès'), backgroundColor: Colors.green, ), ); // Pas besoin de recharger, le ValueListenableBuilder // se rafraîchira automatiquement après la suppression dans Hive } else if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Erreur lors de la suppression du passage'), backgroundColor: Colors.red, ), ); } } catch (e) { debugPrint('Erreur suppression passage: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: $e'), backgroundColor: Colors.red, ), ); } } } // Formater une date String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } // Construction du filtre par mode de règlement Widget _buildPaymentFilter(ThemeData theme) { return 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), hint: const Text('Sélectionner un mode de règlement'), 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; }); } }, ), ), ); } }