import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; // Pour accéder aux instances globales import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; class UserHistoryPage extends StatefulWidget { const UserHistoryPage({super.key}); @override State createState() => _UserHistoryPageState(); } // 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 _UserHistoryPageState extends State { // Liste qui contiendra les passages convertis List> _convertedPassages = []; // Variables pour indiquer l'état de chargement bool _isLoading = true; String _errorMessage = ''; // Statistiques pour l'affichage int _totalSectors = 0; int _sharedMembersCount = 0; // État du tri actuel PassageSortType _currentSort = PassageSortType.dateDesc; @override void initState() { super.initState(); // Charger les passages depuis la box Hive au démarrage _loadPassages(); } // Méthode pour charger les passages depuis le repository Future _loadPassages() async { setState(() { _isLoading = true; _errorMessage = ''; }); try { // Utiliser l'instance globale définie dans app.dart // Utiliser la propriété passages qui gère déjà l'ouverture de la box final List allPassages = passageRepository.passages; debugPrint('Nombre total de passages dans la box: ${allPassages.length}'); // Ne plus filtrer les passages de type 2 - laisser le widget gérer le filtrage List filtered = allPassages; debugPrint('Nombre total de passages disponibles: ${filtered.length}'); // Afficher la distribution des types de passages pour le débogage final Map typeCount = {}; for (var passage in filtered) { typeCount[passage.fkType] = (typeCount[passage.fkType] ?? 0) + 1; } typeCount.forEach((type, count) { debugPrint('Type de passage $type: $count passages'); }); // Afficher la plage de dates pour le débogage if (filtered.isNotEmpty) { // Trier par date pour trouver min et max (exclure les passages sans date) final sortedByDate = List.from(filtered.where((p) => p.passedAt != null)); if (sortedByDate.isNotEmpty) { sortedByDate.sort((a, b) => a.passedAt!.compareTo(b.passedAt!)); final DateTime minDate = sortedByDate.first.passedAt!; final DateTime maxDate = sortedByDate.last.passedAt!; // Log détaillé pour débogage debugPrint( 'Plage de dates des passages: ${minDate.toString()} à ${maxDate.toString()}'); // Afficher les 5 passages les plus anciens et les 5 plus récents pour débogage debugPrint('\n--- 5 PASSAGES LES PLUS ANCIENS ---'); for (int i = 0; i < sortedByDate.length && i < 5; i++) { final p = sortedByDate[i]; debugPrint( 'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}'); } debugPrint('\n--- 5 PASSAGES LES PLUS RÉCENTS ---'); for (int i = sortedByDate.length - 1; i >= 0 && i >= sortedByDate.length - 5; i--) { final p = sortedByDate[i]; debugPrint( 'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}'); } // Vérifier la distribution des passages par mois final Map monthCount = {}; for (var passage in filtered) { // Ignorer les passages sans date if (passage.passedAt != null) { final String monthKey = '${passage.passedAt!.year}-${passage.passedAt!.month.toString().padLeft(2, '0')}'; monthCount[monthKey] = (monthCount[monthKey] ?? 0) + 1; } } debugPrint('\n--- DISTRIBUTION PAR MOIS ---'); final sortedMonths = monthCount.keys.toList()..sort(); for (var month in sortedMonths) { debugPrint('$month: ${monthCount[month]} passages'); } } } // Convertir les modèles en Maps pour l'affichage avec gestion d'erreurs List> passagesMap = []; for (var passage in filtered) { try { final Map passageMap = _convertPassageModelToMap(passage); passagesMap.add(passageMap); } catch (e) { debugPrint('Erreur lors de la conversion du passage en map: $e'); // Ignorer ce passage et continuer } } debugPrint('Nombre de passages après conversion: ${passagesMap.length}'); // Trier par date (plus récent en premier) avec gestion d'erreurs try { passagesMap.sort((a, b) { try { return (b['date'] as DateTime).compareTo(a['date'] as DateTime); } catch (e) { debugPrint('Erreur lors de la comparaison des dates: $e'); return 0; // Garder l'ordre actuel en cas d'erreur } }); } catch (e) { debugPrint('Erreur lors du tri des passages: $e'); // Continuer sans tri en cas d'erreur } // Debug: vérifier la plage de dates après conversion et tri if (passagesMap.isNotEmpty) { debugPrint('\n--- PLAGE DE DATES APRÈS CONVERSION ET TRI ---'); final firstDate = passagesMap.last['date'] as DateTime; final lastDate = passagesMap.first['date'] as DateTime; debugPrint('Premier passage: ${firstDate.toString()}'); debugPrint('Dernier passage: ${lastDate.toString()}'); } // Calculer le nombre de secteurs uniques final Set uniqueSectors = {}; for (var passage in filtered) { if (passage.fkSector != null && passage.fkSector! > 0) { uniqueSectors.add(passage.fkSector!); } } // Compter les membres partagés (autres membres dans la même amicale) int sharedMembers = 0; try { // Utiliser l'instance globale définie dans app.dart final currentUserId = userRepository.getCurrentUser()?.id; final allMembers = membreRepository.membres; // Utiliser la propriété membres // Compter les membres autres que l'utilisateur courant sharedMembers = allMembers.where((membre) => membre.id != currentUserId).length; debugPrint('Nombre de membres partagés: $sharedMembers'); } catch (e) { debugPrint('Erreur lors du comptage des membres: $e'); } setState(() { _convertedPassages = passagesMap; _totalSectors = uniqueSectors.length; _sharedMembersCount = sharedMembers; _isLoading = false; }); } catch (e) { setState(() { _errorMessage = 'Erreur lors du chargement des passages: $e'; _isLoading = false; }); debugPrint(_errorMessage); } } // Convertir un modèle de passage en Map pour l'affichage avec gestion renforcée des erreurs Map _convertPassageModelToMap(PassageModel passage) { try { // Le passage ne peut pas être null en Dart non-nullable, // mais nous gardons cette structure pour faciliter la gestion des erreurs // Construire l'adresse complète avec gestion des erreurs String address = 'Adresse non disponible'; try { address = _buildFullAddress(passage); } catch (e) { debugPrint('Erreur lors de la construction de l\'adresse: $e'); } // Convertir le montant en double avec sécurité double amount = 0.0; try { if (passage.montant.isNotEmpty) { amount = double.parse(passage.montant); } } catch (e) { debugPrint('Erreur de conversion du montant: ${passage.montant}: $e'); } // Récupérer la date avec gestion d'erreur DateTime date; try { date = passage.passedAt ?? DateTime.now(); } catch (e) { debugPrint('Erreur lors de la récupération de la date: $e'); date = DateTime.now(); } // Récupérer le type avec gestion d'erreur int type; try { type = passage.fkType; // Si le type n'est pas dans les types connus, utiliser 1 comme valeur par défaut if (!AppKeys.typesPassages.containsKey(type)) { type = 1; // Type 1 par défaut (Effectué) } } catch (e) { debugPrint('Erreur lors de la récupération du type: $e'); type = 1; // Type 1 par défaut } // Récupérer le type de règlement avec gestion d'erreur int payment; try { payment = passage.fkTypeReglement; // Si le type de règlement n'est pas dans les types connus, utiliser 0 comme valeur par défaut if (!AppKeys.typesReglements.containsKey(payment)) { payment = 0; // Type de règlement inconnu } } catch (e) { debugPrint('Erreur lors de la récupération du type de règlement: $e'); payment = 0; } // Gérer les champs optionnels String name = ''; try { name = passage.name; } catch (e) { debugPrint('Erreur lors de la récupération du nom: $e'); } String notes = ''; try { notes = passage.remarque; } catch (e) { debugPrint('Erreur lors de la récupération des remarques: $e'); } // Vérifier si un reçu est disponible avec gestion d'erreur bool hasReceipt = false; try { hasReceipt = amount > 0 && type == 1 && passage.nomRecu.isNotEmpty; } catch (e) { debugPrint('Erreur lors de la vérification du reçu: $e'); } // Vérifier s'il y a une erreur avec gestion d'erreur bool hasError = false; try { hasError = passage.emailErreur.isNotEmpty; } catch (e) { debugPrint('Erreur lors de la vérification des erreurs: $e'); } // Log pour débogage debugPrint( 'Conversion passage ID: ${passage.id}, Type: $type, Date: $date'); return { 'id': passage.id, // Garder l'ID comme int, pas besoin de toString() 'address': address, 'amount': amount, 'date': date, 'type': type, 'payment': payment, 'name': name, 'notes': notes, 'hasReceipt': hasReceipt, 'hasError': hasError, 'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur 'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget // Ajouter les composants de l'adresse pour le tri 'rue': passage.rue, 'numero': passage.numero, 'rueBis': passage.rueBis, }; } catch (e) { debugPrint('ERREUR CRITIQUE lors de la conversion du passage: $e'); // Retourner un objet valide par défaut pour éviter les erreurs // Récupérer l'ID de l'utilisateur courant pour l'objet par défaut // Utiliser l'instance globale définie dans app.dart final currentUserId = userRepository.getCurrentUser()?.id; return { 'id': 'error', 'address': 'Adresse non disponible', 'amount': 0.0, 'date': DateTime.now(), 'type': 1, // Type 1 par défaut au lieu de 0 'payment': 1, // Payment 1 par défaut au lieu de 0 'name': 'Nom non disponible', 'notes': '', 'hasReceipt': false, 'hasError': true, 'fkUser': currentUserId, // Ajouter l'ID de l'utilisateur courant // Composants de l'adresse pour le tri 'rue': '', 'numero': '', 'rueBis': '', }; } } // 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; } // Construire l'adresse complète à partir des composants String _buildFullAddress(PassageModel passage) { final List addressParts = []; // Numéro et rue if (passage.numero.isNotEmpty) { addressParts.add('${passage.numero} ${passage.rue}'); } else { addressParts.add(passage.rue); } // Complément rue bis if (passage.rueBis.isNotEmpty) { addressParts.add(passage.rueBis); } // Résidence/Bâtiment if (passage.residence.isNotEmpty) { addressParts.add(passage.residence); } // Appartement if (passage.appt.isNotEmpty) { addressParts.add('Appt ${passage.appt}'); } // Niveau if (passage.niveau.isNotEmpty) { addressParts.add('Niveau ${passage.niveau}'); } // Ville if (passage.ville.isNotEmpty) { addressParts.add(passage.ville); } return addressParts.join(', '); } @override void dispose() { super.dispose(); } // Méthode pour afficher les détails d'un passage void _showPassageDetails(Map passage) { // Récupérer les informations du type de passage et du type de règlement final typePassage = AppKeys.typesPassages[passage['type']] as Map; final typeReglement = AppKeys.typesReglements[passage['payment']] as Map; showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Détails du passage'), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildDetailRow('Adresse', passage['address']), _buildDetailRow('Nom', passage['name']), _buildDetailRow('Date', '${passage['date'].day}/${passage['date'].month}/${passage['date'].year}'), _buildDetailRow('Type', typePassage['titre']), _buildDetailRow('Règlement', typeReglement['titre']), _buildDetailRow('Montant', '${passage['amount']}€'), if (passage['notes'] != null && passage['notes'].toString().isNotEmpty) _buildDetailRow('Notes', passage['notes']), if (passage['hasReceipt'] == true) _buildDetailRow('Reçu', 'Disponible'), if (passage['hasError'] == true) _buildDetailRow('Erreur', 'Détectée', isError: true), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), if (passage['hasReceipt'] == true) TextButton( onPressed: () { Navigator.of(context).pop(); _showReceipt(passage); }, child: const Text('Voir le reçu'), ), TextButton( onPressed: () { Navigator.of(context).pop(); _editPassage(passage); }, child: const Text('Modifier'), ), ], ), ); } // Méthode pour éditer un passage void _editPassage(Map passage) { // Implémenter l'ouverture d'un formulaire d'édition // Cette méthode pourrait naviguer vers une page d'édition debugPrint('Édition du passage ${passage['id']}'); // Exemple: Navigator.of(context).push(MaterialPageRoute(builder: (_) => EditPassagePage(passage: passage))); } // Méthode pour afficher un reçu void _showReceipt(Map passage) { // Implémenter l'affichage ou la génération d'un reçu // Cette méthode pourrait générer un PDF et l'afficher debugPrint('Affichage du reçu pour le passage ${passage['id']}'); // Exemple: Navigator.of(context).push(MaterialPageRoute(builder: (_) => ReceiptPage(passage: passage))); } // Helper pour construire une ligne de détails Widget _buildDetailRow(String label, String value, {bool isError = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 100, child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.bold))), Expanded( child: Text( value, style: isError ? const TextStyle(color: Colors.red) : null, ), ), ], ), ); } // Variable pour gérer la recherche final String _searchQuery = ''; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête avec bouton de rafraîchissement Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _isLoading ? 'Historique des passages' : 'Historique des ${_convertedPassages.length} passages${_totalSectors > 0 ? ' ($_totalSectors secteur${_totalSectors > 1 ? 's' : ''})' : ''}', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), if (!_isLoading && _sharedMembersCount > 0) Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( 'Partagés avec $_sharedMembersCount membre${_sharedMembersCount > 1 ? 's' : ''}', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), fontStyle: FontStyle.italic, ), ), ), ], ), ), IconButton( icon: const Icon(Icons.refresh), onPressed: _loadPassages, tooltip: 'Rafraîchir', ), ], ), ], ), ), // Affichage du chargement ou des erreurs if (_isLoading) const Expanded( child: Center( child: CircularProgressIndicator(), ), ) else if (_errorMessage.isNotEmpty) Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.red), const SizedBox(height: 16), Text( 'Erreur de chargement', style: theme.textTheme.titleLarge ?.copyWith(color: Colors.red), ), const SizedBox(height: 8), Text(_errorMessage), const SizedBox(height: 16), ElevatedButton( onPressed: _loadPassages, child: const Text('Réessayer'), ), ], ), ), ) // Utilisation du widget PassagesListWidget pour afficher la liste des passages else Expanded( child: Container( color: Colors.transparent, child: Column( children: [ // Widget de liste des passages avec ValueListenableBuilder Expanded( child: ValueListenableBuilder( valueListenable: Hive.box(AppKeys.passagesBoxName).listenable(), builder: (context, Box passagesBox, child) { // Reconvertir les passages à chaque changement final List allPassages = passagesBox.values.toList(); // Appliquer le même filtrage et conversion List> passagesMap = []; for (var passage in allPassages) { try { final Map passageMap = _convertPassageModelToMap(passage); passagesMap.add(passageMap); } catch (e) { debugPrint('Erreur lors de la conversion du passage en map: $e'); } } // Appliquer le tri sélectionné passagesMap = _sortPassages(passagesMap); 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 context) { 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.colorScheme.primary : theme.colorScheme.onSurface.withOpacity(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.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.colorScheme.primary : theme.colorScheme.onSurface.withOpacity(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.colorScheme.primary, ), ], ), passages: passagesMap, showFilters: true, showSearch: true, showActions: true, initialSearchQuery: _searchQuery, initialTypeFilter: 'Tous', initialPaymentFilter: 'Tous', excludePassageTypes: const [], filterByUserId: userRepository.getCurrentUser()?.id, key: const ValueKey('user_passages_list'), // Le widget gère maintenant le flux conditionnel par défaut onPassageSelected: null, onDetailsView: (passage) { debugPrint('Affichage des détails: ${passage['id']}'); _showPassageDetails(passage); }, onPassageEdit: (passage) { debugPrint('Modification du passage: ${passage['id']}'); _editPassage(passage); }, onReceiptView: (passage) { debugPrint('Affichage du reçu pour le passage: ${passage['id']}'); _showReceipt(passage); }, onPassageDelete: (passage) { // Pas besoin de recharger, le ValueListenableBuilder // se rafraîchira automatiquement après la suppression }, ); }, ), ), ], ), ), ), ], ), ), ); } }