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/services/current_user_service.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; 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/presentation/widgets/app_scaffold.dart'; import 'package:geosector_app/presentation/widgets/btn_passages.dart'; /// Page d'historique unifiée utilisant AppScaffold class HistoryPage extends StatelessWidget { /// ID du membre à filtrer (optionnel, pour les admins) final int? memberId; const HistoryPage({ super.key, this.memberId, }); @override Widget build(BuildContext context) { return AppScaffold( selectedIndex: 1, // Index de la page History dans la navigation pageTitle: 'Historique', body: HistoryContent(memberId: memberId), ); } } /// Contenu de la page historique unifié pour admin et user class HistoryContent extends StatefulWidget { /// ID du membre à filtrer (optionnel, pour les admins) final int? memberId; const HistoryContent({ super.key, this.memberId, }); @override State createState() => _HistoryContentState(); } class _HistoryContentState extends State { // Détection du rôle et permissions late final bool isAdmin; late final int currentUserId; // users.id (table centrale) late final int? currentOpeUserId; // ope_users.id (pour comparaisons avec passages) late final bool canDeletePassages; // Permission de suppression pour les users // Filtres String _selectedTypeFilter = 'Tous les types'; String _searchQuery = ''; int? selectedTypeId; int? _selectedMemberId; // null = "Tous" (admin uniquement) int? _selectedSectorId; // null = "Tous" (admin uniquement) // Contrôleur de recherche final TextEditingController _searchController = TextEditingController(); // Passages originaux pour l'édition List _originalPassages = []; List _filteredPassages = []; // État de chargement bool _isLoading = true; String _errorMessage = ''; @override void initState() { super.initState(); // Déterminer le rôle et les permissions de l'utilisateur (prend en compte le mode d'affichage) final currentUser = userRepository.getCurrentUser(); isAdmin = CurrentUserService.instance.shouldShowAdminUI; currentUserId = currentUser?.id ?? 0; currentOpeUserId = currentUser?.opeUserId; // ID dans ope_users pour comparaisons avec passages // Vérifier la permission de suppression pour les users bool userCanDelete = false; if (!isAdmin && currentUser != null && currentUser.fkEntite != null) { final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); if (userAmicale != null) { userCanDelete = userAmicale.chkUserDeletePass; } } canDeletePassages = isAdmin || userCanDelete; // Charger les filtres présélectionnés depuis Hive _loadPreselectedFilters(); // Initialiser les données _initializeData(); } @override void dispose() { _searchController.dispose(); super.dispose(); } // Callback pour gérer les clics sur les boutons de type de passage void _handleTypeSelected(int? typeId) { setState(() { // Réinitialiser la recherche _searchQuery = ''; _searchController.clear(); // Appliquer le filtre de type if (typeId == null) { // Tous les passages _selectedTypeFilter = 'Tous les types'; selectedTypeId = null; } else { // Type spécifique selectedTypeId = typeId; final typeInfo = AppKeys.typesPassages[typeId]; if (typeInfo != null) { _selectedTypeFilter = typeInfo['titre'] as String; } } }); // Appliquer les filtres _applyFilters(); } // Charger les filtres présélectionnés depuis Hive Future _loadPreselectedFilters() async { try { if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) { await Hive.openBox(AppKeys.settingsBoxName); } final settingsBox = Hive.box(AppKeys.settingsBoxName); // Charger le type de passage sélectionné final typeId = settingsBox.get('history_selectedTypeId'); if (typeId != null && typeId is int) { setState(() { selectedTypeId = typeId; final typeInfo = AppKeys.typesPassages[typeId]; if (typeInfo != null) { _selectedTypeFilter = typeInfo['titre'] as String; } }); // Supprimer le typeId de Hive après l'avoir utilisé settingsBox.delete('history_selectedTypeId'); debugPrint('HistoryPage: Type de passage présélectionné: $typeId'); } } catch (e) { debugPrint('Erreur lors du chargement des filtres: $e'); } } // Initialiser les données void _initializeData() { try { setState(() { _isLoading = true; _errorMessage = ''; }); // Charger les passages final currentOperation = userRepository.getCurrentOperation(); if (currentOperation != null) { if (isAdmin) { // Admin : tous les passages de l'opération _originalPassages = passageRepository.getPassagesByOperation(currentOperation.id); } else { // User : logique spéciale selon le type de passage final allPassages = passageRepository.getPassagesByOperation(currentOperation.id); _originalPassages = allPassages.where((p) { // Type 2 (À finaliser) : afficher TOUS les passages if (p.fkType == 2) { return true; } // Autres types : seulement les passages de l'utilisateur return p.fkUser == currentOpeUserId; }).toList(); } debugPrint('Nombre de passages récupérés: ${_originalPassages.length}'); // Initialiser les passages filtrés avec tous les passages _filteredPassages = List.from(_originalPassages); // Appliquer les filtres initiaux après le chargement _applyFilters(); } setState(() { _isLoading = false; }); } catch (e) { setState(() { _isLoading = false; _errorMessage = 'Erreur lors du chargement des données: $e'; }); debugPrint('Erreur lors de l\'initialisation des données: $e'); } } /// Appliquer les filtres aux données void _applyFilters() { debugPrint('HistoryPage: Application des filtres'); debugPrint(' Type: $_selectedTypeFilter'); debugPrint(' Recherche: $_searchQuery'); // Appliquer les filtres aux passages originaux List filteredPassages = _originalPassages.where((passage) { // Filtre par type de passage if (_selectedTypeFilter != 'Tous les types') { final typeInfo = AppKeys.typesPassages.entries.firstWhere( (entry) => entry.value['titre'] == _selectedTypeFilter, orElse: () => const MapEntry(-1, {'titre': '', 'couleur': ''}), ); if (typeInfo.key == -1 || passage.fkType != typeInfo.key) { return false; } } // Filtre par membre (admin uniquement) if (isAdmin && _selectedMemberId != null) { if (passage.fkUser != _selectedMemberId) { return false; } } // Filtre par secteur (admin uniquement) if (isAdmin && _selectedSectorId != null) { if (passage.fkSector != _selectedSectorId) { return false; } } // Filtre par recherche textuelle if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); // Construire l'adresse complète pour la recherche final fullAddress = '${passage.numero} ${passage.rueBis} ${passage.rue} ${passage.residence} ${passage.ville}' .toLowerCase() .trim(); // Ajouter le nom et l'email final name = passage.name.toLowerCase(); final email = passage.email.toLowerCase(); // Vérifier si la recherche correspond à l'adresse, au nom ou à l'email if (!fullAddress.contains(query) && !name.contains(query) && !email.contains(query)) { return false; } } return true; }).toList(); // Mettre à jour les données filtrées setState(() { _filteredPassages = filteredPassages; }); debugPrint('HistoryPage: ${filteredPassages.length} passages filtrés sur ${_originalPassages.length}'); } @override Widget build(BuildContext context) { // Le contenu sans scaffold (AppScaffold est déjà dans HistoryPage) if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_errorMessage.isNotEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 64, color: Colors.red), const SizedBox(height: 16), Text(_errorMessage, style: const TextStyle(fontSize: 16)), const SizedBox(height: 16), ElevatedButton( onPressed: _initializeData, child: const Text('Réessayer'), ), ], ), ); } return _buildContent(); } Widget _buildContent() { final screenWidth = MediaQuery.of(context).size.width; final isDesktop = screenWidth > 800; return Column( children: [ // 0. BtnPassages collé en haut/gauche/droite BtnPassages( onTypeSelected: _handleTypeSelected, selectedTypeId: selectedTypeId, ), // 1. Barre de recherche Padding( padding: EdgeInsets.symmetric( horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, vertical: AppTheme.spacingS, ), child: Row( children: [ // Barre de recherche Expanded( flex: 3, child: TextFormField( controller: _searchController, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), hintText: 'Rechercher...', prefixIcon: Icon(Icons.search, size: 20), isDense: true, ), onChanged: (String value) { setState(() { _searchQuery = value; }); _applyFilters(); }, ), ), // Filtres admin uniquement if (isAdmin) ...[ const SizedBox(width: 8), // Filtre par membre Expanded( flex: 2, child: _buildMemberDropdown(), ), const SizedBox(width: 8), // Filtre par secteur Expanded( flex: 2, child: _buildSectorDropdown(), ), ], ], ), ), // 2. Liste des passages - EXPANDED pour prendre tout l'espace restant Expanded( child: Padding( padding: EdgeInsets.symmetric( horizontal: isDesktop ? AppTheme.spacingL : AppTheme.spacingS, ), child: Card( elevation: 2, child: PassagesListWidget( passages: _convertPassagesToMaps(), showActions: true, // Actions disponibles pour tous (avec vérification des droits) showAddButton: true, // Bouton + pour tous onPassageEdit: _handlePassageEditMap, // Tous peuvent éditer (avec vérification) onPassageDelete: canDeletePassages ? _handlePassageDeleteMap : null, // Selon permissions onAddPassage: () async { await _showPassageFormDialog(context); }, filteredPassageType: _selectedTypeFilter != 'Tous les types' ? _selectedTypeFilter : null, ), ), ), ), ], ); } // Afficher le formulaire de création de passage Future _showPassageFormDialog(BuildContext context) async { await showDialog( context: context, builder: (context) => PassageFormDialog( title: 'Nouveau passage', readOnly: false, passageRepository: passageRepository, userRepository: userRepository, operationRepository: operationRepository, amicaleRepository: amicaleRepository, onSuccess: () { _initializeData(); // Recharger les données }, ), ); } // Méthode helper pour convertir une valeur potentiellement null en String String _safeString(dynamic value) { if (value == null) return ''; return value.toString(); } // Convertir les passages en Map pour PassagesListWidget List> _convertPassagesToMaps() { try { // Utiliser les passages filtrés var passages = _filteredPassages.toList(); // Convertir chaque passage en Map final maps = >[]; for (final passage in passages) { try { // Construire l'adresse complète final fullAddress = '${_safeString(passage.numero)} ${_safeString(passage.rue)} ${_safeString(passage.ville)}'.trim(); // Convertir le montant en double (normalement un String dans PassageModel) double? montantDouble = 0.0; final montantStr = _safeString(passage.montant); if (montantStr.isNotEmpty) { montantDouble = double.tryParse(montantStr) ?? 0.0; } // Gestion de la date selon le type de passage // Pour les passages "À finaliser" (type 2), la date peut être null DateTime? passageDate = passage.passedAt; final map = { 'id': passage.id, 'fk_operation': passage.fkOperation, 'fk_sector': passage.fkSector, 'fk_user': passage.fkUser, 'fkUser': passage.fkUser, 'type': passage.fkType, 'fk_type': passage.fkType, 'payment': passage.fkTypeReglement, 'fk_type_reglement': passage.fkTypeReglement, 'fk_adresse': _safeString(passage.fkAdresse), 'address': fullAddress.isNotEmpty ? fullAddress : 'Adresse non renseignée', 'date': passageDate, 'datePassage': passageDate, 'passed_at': passage.passedAt?.toIso8601String(), 'passedAt': passageDate, 'numero': _safeString(passage.numero), 'rue': _safeString(passage.rue), 'rue_bis': _safeString(passage.rueBis), 'ville': _safeString(passage.ville), 'residence': _safeString(passage.residence), 'fk_habitat': passage.fkHabitat, 'appt': _safeString(passage.appt), 'niveau': _safeString(passage.niveau), 'gps_lat': _safeString(passage.gpsLat), 'gps_lng': _safeString(passage.gpsLng), 'nom_recu': _safeString(passage.nomRecu), 'remarque': _safeString(passage.remarque), 'notes': _safeString(passage.remarque), 'montant': _safeString(passage.montant), 'amount': montantDouble, 'email_erreur': _safeString(passage.emailErreur), 'nb_passages': passage.nbPassages, 'name': _safeString(passage.name), 'email': _safeString(passage.email), 'phone': _safeString(passage.phone), 'fullAddress': fullAddress, }; maps.add(map); } catch (e) { debugPrint('Erreur lors de la conversion du passage ${passage.id}: $e'); } } debugPrint('${maps.length} passages convertis avec succès'); return maps; } catch (e) { debugPrint('Erreur lors de la conversion des passages en Maps: $e'); return []; } } // Gérer l'édition d'un passage depuis le Map void _handlePassageEditMap(Map passageMap) { final passageId = passageMap['id'] as int; final passage = _originalPassages.firstWhere( (p) => p.id == passageId, orElse: () => PassageModel.fromJson(passageMap), ); // Vérifier les permissions : // - Admin peut tout éditer // - User peut éditer ses propres passages // - Type 2 (À finaliser) : éditable par tous les utilisateurs if (isAdmin || passage.fkUser == currentOpeUserId || passage.fkType == 2) { _handlePassageEdit(passage); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Vous ne pouvez éditer que vos propres passages'), backgroundColor: Colors.orange, ), ); } } // Gérer la suppression d'un passage depuis le Map void _handlePassageDeleteMap(Map passageMap) { final passageId = passageMap['id'] as int; final passage = _originalPassages.firstWhere( (p) => p.id == passageId, orElse: () => PassageModel.fromJson(passageMap), ); // Vérifier les permissions : admin peut tout supprimer, user seulement ses propres passages si autorisé if (isAdmin || (canDeletePassages && passage.fkUser == currentOpeUserId)) { _handlePassageDelete(passage); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( !canDeletePassages ? 'Vous n\'êtes pas autorisé à supprimer des passages' : 'Vous ne pouvez supprimer que vos propres passages' ), backgroundColor: Colors.orange, ), ); } } // Gérer l'édition d'un passage void _handlePassageEdit(PassageModel passage) async { await showDialog( context: context, builder: (context) => PassageFormDialog( passage: passage, title: 'Modifier le passage', readOnly: false, passageRepository: passageRepository, userRepository: userRepository, operationRepository: operationRepository, amicaleRepository: amicaleRepository, onSuccess: () { _initializeData(); // Recharger les données }, ), ); } // Gérer la suppression d'un passage void _handlePassageDelete(PassageModel passage) async { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Confirmer la suppression'), content: Text( 'Êtes-vous sûr de vouloir supprimer le passage chez ${passage.name} ?', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Supprimer'), ), ], ), ); if (confirm == true) { try { await passageRepository.deletePassage(passage.id); _initializeData(); // Recharger les données if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Passage supprimé avec succès'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors de la suppression: $e'), backgroundColor: Colors.red, ), ); } } } } /// Construit le dropdown de sélection de membre (admin uniquement) Widget _buildMemberDropdown() { // Récupérer les membres uniques depuis les passages de l'opération courante final memberIds = {}; final memberNames = {}; for (final passage in _originalPassages) { if (!memberIds.contains(passage.fkUser)) { memberIds.add(passage.fkUser); // Utiliser le nom du passage (qui contient prenom + nom) memberNames[passage.fkUser] = passage.name.isNotEmpty ? passage.name : 'Membre #${passage.fkUser}'; } } // Trier par nom final sortedMembers = memberIds.toList() ..sort((a, b) => (memberNames[a] ?? '').compareTo(memberNames[b] ?? '')); return DropdownButtonFormField( value: _selectedMemberId, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), isDense: true, labelText: 'Membre', prefixIcon: Icon(Icons.person, size: 20), ), items: [ const DropdownMenuItem( value: null, child: Text('Tous'), ), ...sortedMembers.map((memberId) { return DropdownMenuItem( value: memberId, child: Text( memberNames[memberId] ?? 'Membre #$memberId', overflow: TextOverflow.ellipsis, ), ); }), ], onChanged: (int? newValue) { setState(() { _selectedMemberId = newValue; }); _applyFilters(); }, ); } /// Construit le dropdown de sélection de secteur (admin uniquement) Widget _buildSectorDropdown() { // Récupérer les secteurs uniques depuis les passages de l'opération courante final sectorIds = {}; for (final passage in _originalPassages) { if (passage.fkSector != null && !sectorIds.contains(passage.fkSector)) { sectorIds.add(passage.fkSector!); } } // Récupérer les noms des secteurs depuis le repository final allSectors = sectorRepository.getAllSectors(); final sectorNames = {}; for (final sector in allSectors) { if (sectorIds.contains(sector.id)) { sectorNames[sector.id] = sector.libelle; } } // Trier par nom final sortedSectors = sectorIds.toList() ..sort((a, b) => (sectorNames[a] ?? '').compareTo(sectorNames[b] ?? '')); return DropdownButtonFormField( value: _selectedSectorId, decoration: const InputDecoration( border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), isDense: true, labelText: 'Secteur', prefixIcon: Icon(Icons.location_on, size: 20), ), items: [ const DropdownMenuItem( value: null, child: Text('Tous'), ), ...sortedSectors.map((sectorId) { return DropdownMenuItem( value: sectorId, child: Text( sectorNames[sectorId] ?? 'Secteur #$sectorId', overflow: TextOverflow.ellipsis, ), ); }), ], onChanged: (int? newValue) { setState(() { _selectedSectorId = newValue; }); _applyFilters(); }, ); } }