import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales 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/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/theme/app_theme.dart'; import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.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({Key? key}) : super(key: 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 = 'Dernier mois'; // Période par défaut DateTimeRange? selectedDateRange; // IDs pour les filtres int? selectedSectorId; int? selectedUserId; // Listes pour les filtres List _sectors = []; List _users = []; // Repositories late PassageRepository _passageRepository; late SectorRepository _sectorRepository; late UserRepository _userRepository; // Passages formatés List> _formattedPassages = []; // État de chargement bool _isLoading = true; String _errorMessage = ''; @override void initState() { super.initState(); // Initialiser les filtres _initializeFilters(); } @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; // Charger les secteurs et les utilisateurs _loadSectorsAndUsers(); // Charger les passages _loadPassages(); } catch (e) { setState(() { _isLoading = false; _errorMessage = 'Erreur lors du chargement des repositories: $e'; }); } } // Charger les secteurs et les utilisateurs void _loadSectorsAndUsers() { 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 utilisateurs _users = _userRepository.getAllUsers(); debugPrint('Nombre d\'utilisateurs récupérés: ${_users.length}'); } catch (e) { debugPrint('Erreur lors du chargement des secteurs et utilisateurs: $e'); } } // Charger les passages void _loadPassages() { setState(() { _isLoading = true; }); try { // Récupérer les passages final List allPassages = _passageRepository.getAllPassages(); // Convertir les passages en format attendu par PassagesListWidget _formattedPassages = _formatPassagesForWidget( allPassages, _sectorRepository, _userRepository); 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 : dernier mois selectedPeriod = 'Dernier mois'; // Plage de dates par défaut : dernier mois final DateTime now = DateTime.now(); final DateTime oneMonthAgo = DateTime(now.year, now.month - 1, now.day); selectedDateRange = DateTimeRange(start: oneMonthAgo, end: now); } // 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: Container(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: Container(width: double.infinity, height: double.infinity), ), ), // Contenu de la page Padding( padding: const EdgeInsets.all(16.0), 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 Expanded( child: PassagesListWidget( passages: _formattedPassages, showFilters: true, showSearch: true, showActions: true, initialSearchQuery: searchQuery, initialTypeFilter: selectedType, initialPaymentFilter: selectedPaymentMethod, // Exclure les passages de type 2 (À finaliser) excludePassageTypes: [2], // Filtres par utilisateur et secteur filterByUserId: selectedUserId, filterBySectorId: selectedSectorId, // Période par défaut (dernier mois) periodFilter: 'lastMonth', // Plage de dates personnalisée si définie dateRange: selectedDateRange, onPassageSelected: (passage) { _showDetailsDialog(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: Container(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, UserRepository userRepository) { return passages.map((passage) { // Récupérer le secteur associé au passage final SectorModel? sector = sectorRepository.getSectorById(passage.fkSector); // Récupérer l'utilisateur associé au passage final UserModel? user = userRepository.getUserById(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, 'date': passage.passedAt, 'address': address, 'fkSector': passage.fkSector, 'sector': sector?.libelle ?? 'Secteur inconnu', 'fkUser': passage.fkUser, 'user': user?.name ?? 'Utilisateur 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, // Ajouter d'autres champs nécessaires 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'), ), ], ), ); } 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'), ), ], ), ); } 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), // Disposition des filtres en fonction de la taille de l'écran isDesktop ? Row( children: [ // Filtre par secteur Expanded( child: _buildSectorFilter(theme, _sectors), ), const SizedBox(width: 16), // Filtre par utilisateur Expanded( child: _buildUserFilter(theme, _users), ), const SizedBox(width: 16), // Filtre par période Expanded( child: _buildPeriodFilter(theme), ), ], ) : Column( children: [ // Filtre par secteur _buildSectorFilter(theme, _sectors), const SizedBox(height: 16), // Filtre par utilisateur _buildUserFilter(theme, _users), const SizedBox(height: 16), // Filtre par période _buildPeriodFilter(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, ), ); }).toList(), ], 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 utilisateur Widget _buildUserFilter(ThemeData theme, List users) { // Vérifier si la liste des utilisateurs est vide ou si selectedUser n'est pas dans la liste bool isSelectedUserValid = selectedUser == 'Tous' || users.any((u) => (u.name ?? 'Utilisateur inconnu') == 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( 'Utilisateur', 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 utilisateurs'), ), ...users.map((user) { // S'assurer que user.name n'est pas null final String userName = user.name ?? 'Utilisateur inconnu'; return DropdownMenuItem( value: userName, child: Text( userName, overflow: TextOverflow.ellipsis, ), ); }).toList(), ], onChanged: (String? value) { if (value != null) { if (value == 'Tous') { _updateUserFilter('Tous', null); } else { try { // Trouver l'utilisateur correspondant final user = users.firstWhere( (u) => (u.name ?? 'Utilisateur inconnu') == value, orElse: () => users.isNotEmpty ? users.first : throw Exception('Liste d\'utilisateurs vide'), ); // S'assurer que user.name et user.id ne sont pas null final String userName = user.name ?? 'Utilisateur inconnu'; final int? userId = user.id; _updateUserFilter(userName, userId); } catch (e) { debugPrint( 'Erreur lors de la sélection de l\'utilisateur: $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, ), ), ], ), ), ], ); } void _showResendConfirmation(BuildContext context, int passageId) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Renvoyer le reçu'), content: Text( 'Êtes-vous sûr de vouloir renvoyer le reçu du passage #$passageId ?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { // Action pour renvoyer le reçu Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Reçu du passage #$passageId renvoyé avec succès'), backgroundColor: Colors.green, ), ); }, child: const Text('Renvoyer'), ), ], ), ); } }