import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.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/data/models/user_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 du tri actuel PassageSortType _currentSort = PassageSortType.dateDesc; // Filtres présélectionnés depuis une autre page int? selectedSectorId; String selectedSector = 'Tous'; String selectedType = 'Tous'; // 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 présélectionné selectedSectorId = null; selectedSector = 'Tous'; selectedType = 'Tous'; } // 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 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) { // Padding responsive : réduit sur mobile pour maximiser l'espace final screenWidth = MediaQuery.of(context).size.width; final horizontalPadding = screenWidth < 600 ? 8.0 : 16.0; final verticalPadding = 16.0; return Padding( padding: EdgeInsets.symmetric( horizontal: horizontalPadding, vertical: verticalPadding, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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(); // Convertir et formater les passages final formattedPassages = _formatPassagesForWidget( allPassages, _sectorRepository, _membreRepository); // Récupérer les UserModel depuis les MembreModel final users = _membres.map((membre) { return userRepository.getUserById(membre.id); }).where((user) => user != null).toList(); return PassagesListWidget( // Données passages: formattedPassages, // Activation des filtres showFilters: true, showSearch: true, showTypeFilter: true, showPaymentFilter: true, showSectorFilter: true, showUserFilter: true, showPeriodFilter: true, // Données pour les filtres sectors: _sectors, members: users.cast(), // Bouton d'ajout showAddButton: true, 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, ), ], ), // Actions 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(), ], ), ); } // 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}'; } }