import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:go_router/go_router.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/membre_model.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/repositories/operation_repository.dart'; import 'package:geosector_app/core/services/current_user_service.dart'; import 'package:geosector_app/app.dart'; /// Widget affichant un tableau détaillé des membres avec leurs statistiques de passages /// Uniquement visible sur plateforme Web class MembersBoardPassages extends StatefulWidget { final String title; final double? height; const MembersBoardPassages({ super.key, this.title = 'Détails par membre', this.height, }); @override State createState() => _MembersBoardPassagesState(); } class _MembersBoardPassagesState extends State { // Repository pour récupérer l'opération courante uniquement final OperationRepository _operationRepository = operationRepository; // Variables pour le tri (valeurs par défaut, seront restaurées depuis settings) int? _sortColumnIndex; bool _sortAscending = true; @override void initState() { super.initState(); _loadSortSettings(); } // Charger les paramètres de tri depuis la box settings void _loadSortSettings() { try { if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); _sortColumnIndex = settingsBox.get('membersBoardSortColumn', defaultValue: 2); // 2 = Effectués par défaut _sortAscending = settingsBox.get('membersBoardSortAscending', defaultValue: false); // Descendant par défaut } } catch (e) { debugPrint('Erreur lors du chargement des paramètres de tri: $e'); // Valeurs par défaut en cas d'erreur _sortColumnIndex = 2; _sortAscending = false; } } // Sauvegarder les paramètres de tri dans la box settings void _saveSortSettings() { try { if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); settingsBox.put('membersBoardSortColumn', _sortColumnIndex); settingsBox.put('membersBoardSortAscending', _sortAscending); } } catch (e) { debugPrint('Erreur lors de la sauvegarde des paramètres de tri: $e'); } } // Vérifier si le type Lot doit être affiché bool _shouldShowLotType() { final currentUser = CurrentUserService.instance.currentUser; if (currentUser != null && currentUser.fkEntite != null) { final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); if (userAmicale != null) { return userAmicale.chkLotActif; } } return true; // Par défaut, on affiche } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // En-tête de la card Container( padding: const EdgeInsets.all(AppTheme.spacingM), decoration: BoxDecoration( color: theme.colorScheme.primary.withOpacity(0.05), borderRadius: const BorderRadius.only( topLeft: Radius.circular(AppTheme.borderRadiusMedium), topRight: Radius.circular(AppTheme.borderRadiusMedium), ), ), child: Row( children: [ Icon( Icons.people_outline, color: theme.colorScheme.primary, size: 24, ), const SizedBox(width: AppTheme.spacingS), Text( widget.title, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), ], ), ), // Corps avec le tableau ValueListenableBuilder>( valueListenable: Hive.box(AppKeys.membresBoxName).listenable(), builder: (context, membresBox, child) { final membres = membresBox.values.toList(); // Récupérer l'opération courante final currentOperation = _operationRepository.getCurrentOperation(); if (currentOperation == null) { return const Center( child: Padding( padding: EdgeInsets.all(AppTheme.spacingL), child: Text('Aucune opération en cours'), ), ); } // Trier les membres selon la colonne sélectionnée _sortMembers(membres, currentOperation.id); // Construire les lignes : TOTAL en première position + détails membres final allRows = [ _buildTotalRow(membres, currentOperation.id, theme), ..._buildRows(membres, currentOperation.id, theme), ]; // Afficher le tableau complet sans scroll interne return SizedBox( width: double.infinity, // Prendre toute la largeur disponible child: Theme( data: Theme.of(context).copyWith( dataTableTheme: DataTableThemeData( headingRowColor: WidgetStateProperty.resolveWith( (Set states) { return theme.colorScheme.primary.withOpacity(0.08); }, ), dataRowColor: WidgetStateProperty.resolveWith( (Set states) { if (states.contains(WidgetState.selected)) { return theme.colorScheme.primary.withOpacity(0.08); } return null; }, ), ), ), child: DataTable( columnSpacing: 4, // Espacement minimal entre colonnes horizontalMargin: 4, // Marges horizontales minimales headingRowHeight: 42, // Hauteur de l'en-tête optimisée dataRowMinHeight: 42, dataRowMaxHeight: 42, // Utiliser les flèches natives de DataTable sortColumnIndex: _sortColumnIndex, sortAscending: _sortAscending, columns: _buildColumns(theme), rows: allRows, ), ), ); }, ), ], ), ); } /// Trie les membres selon la colonne sélectionnée void _sortMembers(List membres, int operationId) { if (_sortColumnIndex == null) { // Tri par défaut : par nom membres.sort((a, b) { final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim(); final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim(); return nameA.compareTo(nameB); }); return; } final passageBox = Hive.box(AppKeys.passagesBoxName); final allPassages = passageBox.values.where((p) => p.fkOperation == operationId).toList(); final showLotType = _shouldShowLotType(); // Fonction helper pour obtenir les stats d'un membre Map getMemberStats(MembreModel membre) { final memberPassages = allPassages.where((p) => p.fkUser == membre.opeUserId).toList(); int totalCount = memberPassages.length; int effectueCount = 0; double effectueMontant = 0.0; int aFinaliserCount = 0; int refuseCount = 0; int donCount = 0; int lotsCount = 0; int videCount = 0; for (final passage in memberPassages) { switch (passage.fkType) { case 1: effectueCount++; if (passage.montant.isNotEmpty) { effectueMontant += double.tryParse(passage.montant) ?? 0.0; } break; case 2: aFinaliserCount++; break; case 3: refuseCount++; break; case 4: donCount++; break; case 5: if (showLotType) lotsCount++; break; case 6: videCount++; break; } } double montantMoyen = effectueCount > 0 ? effectueMontant / effectueCount : 0.0; final memberSectorIds = memberPassages.where((p) => p.fkSector != null).map((p) => p.fkSector!).toSet(); final sectorCount = memberSectorIds.length; final passagesNonAFinaliser = totalCount - aFinaliserCount; double tauxAvancement = totalCount > 0 ? passagesNonAFinaliser / totalCount : 0.0; return { 'total': totalCount, 'effectue': effectueCount, 'effectueMontant': effectueMontant, 'montantMoyen': montantMoyen, 'aFinaliser': aFinaliserCount, 'refuse': refuseCount, 'don': donCount, 'lots': lotsCount, 'vide': videCount, 'tauxAvancement': tauxAvancement, 'secteurs': sectorCount, }; } membres.sort((a, b) { final statsA = getMemberStats(a); final statsB = getMemberStats(b); int result = 0; switch (_sortColumnIndex) { case 0: // Nom final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim(); final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim(); result = nameA.compareTo(nameB); break; case 1: // Total result = statsA['total'].compareTo(statsB['total']); break; case 2: // Effectués result = statsA['effectue'].compareTo(statsB['effectue']); break; case 3: // Montant moyen result = statsA['montantMoyen'].compareTo(statsB['montantMoyen']); break; case 4: // À finaliser result = statsA['aFinaliser'].compareTo(statsB['aFinaliser']); break; case 5: // Refusés result = statsA['refuse'].compareTo(statsB['refuse']); break; case 6: // Dons result = statsA['don'].compareTo(statsB['don']); break; case 7: // Lots (si affiché) if (showLotType) { result = statsA['lots'].compareTo(statsB['lots']); } else { result = statsA['vide'].compareTo(statsB['vide']); } break; case 8: // Vides ou Taux d'avancement (dépend si Lots affiché) if (showLotType) { result = statsA['vide'].compareTo(statsB['vide']); } else { result = statsA['tauxAvancement'].compareTo(statsB['tauxAvancement']); } break; case 9: // Taux d'avancement ou Secteurs (dépend si Lots affiché) if (showLotType) { result = statsA['tauxAvancement'].compareTo(statsB['tauxAvancement']); } else { result = statsA['secteurs'].compareTo(statsB['secteurs']); } break; case 10: // Secteurs (si Lots affiché) result = statsA['secteurs'].compareTo(statsB['secteurs']); break; } return _sortAscending ? result : -result; }); } /// Helper pour construire le texte du header Widget _buildHeaderText(String text, int columnIndex, TextStyle? style) { return Text( text, style: style, ); } /// Helper pour gérer le tri de colonne void _handleSort(int columnIndex) { setState(() { if (_sortColumnIndex == columnIndex) { // Même colonne : basculer l'ordre _sortAscending = !_sortAscending; } else { // Nouvelle colonne : tri ascendant par défaut _sortColumnIndex = columnIndex; _sortAscending = true; } // Sauvegarder les nouveaux paramètres de tri _saveSortSettings(); }); } /// Construit les colonnes du tableau List _buildColumns(ThemeData theme) { // Utilise le thème pour une meilleure lisibilité final headerStyle = theme.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, ) ?? const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ); final showLotType = _shouldShowLotType(); final columns = [ // Nom DataColumn( label: Expanded( child: _buildHeaderText('Nom', 0, headerStyle), ), onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Total DataColumn( label: Expanded( child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.route, size: 16, color: theme.colorScheme.primary, ), const SizedBox(width: 4), _buildHeaderText('Total', 1, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Effectués DataColumn( label: Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), color: Colors.green.withOpacity(0.2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.task_alt, size: 16, color: Colors.green, ), const SizedBox(width: 4), _buildHeaderText('Effectués', 2, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Montant moyen DataColumn( label: Expanded( child: Center( child: _buildHeaderText('Moy./passage', 3, headerStyle), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // À finaliser DataColumn( label: Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), color: Colors.orange.withOpacity(0.2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.refresh, size: 16, color: Colors.orange, ), const SizedBox(width: 4), _buildHeaderText('À finaliser', 4, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Refusés DataColumn( label: Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), color: Colors.red.withOpacity(0.2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.block, size: 16, color: Colors.red, ), const SizedBox(width: 4), _buildHeaderText('Refusés', 5, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Dons DataColumn( label: Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), color: Colors.lightBlue.withOpacity(0.2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.volunteer_activism, size: 16, color: Colors.lightBlue, ), const SizedBox(width: 4), _buildHeaderText('Dons', 6, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Lots - affiché seulement si chkLotActif = true if (showLotType) DataColumn( label: Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), color: Colors.blue.withOpacity(0.2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.layers, size: 16, color: Colors.blue, ), const SizedBox(width: 4), _buildHeaderText('Lots', 7, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Vides DataColumn( label: Expanded( child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), color: Colors.grey.withOpacity(0.2), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.home_outlined, size: 16, color: Colors.grey, ), const SizedBox(width: 4), _buildHeaderText('Vides', showLotType ? 8 : 7, headerStyle), ], ), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Taux d'avancement DataColumn( label: Expanded( child: Center( child: _buildHeaderText('Avancement', showLotType ? 9 : 8, headerStyle), ), ), onSort: (columnIndex, _) => _handleSort(columnIndex), ), // Secteurs DataColumn( label: Expanded( child: Center( child: _buildHeaderText('Secteurs', showLotType ? 10 : 9, headerStyle), ), ), numeric: true, onSort: (columnIndex, _) => _handleSort(columnIndex), ), ]; return columns; } /// Navigation vers l'historique avec un type de passage void _navigateToHistoryWithType(int typeId, {int? memberId}) { final settingsBox = Hive.box(AppKeys.settingsBoxName); // Réinitialiser TOUS les filtres settingsBox.delete('history_selectedSectorId'); settingsBox.delete('history_selectedSectorName'); settingsBox.delete('history_selectedPaymentTypeId'); settingsBox.delete('history_startDate'); settingsBox.delete('history_endDate'); // Sélectionner le type de passage settingsBox.put('history_selectedTypeId', typeId); // Sélectionner le membre si spécifié, sinon réinitialiser (Tous les membres) if (memberId != null) { settingsBox.put('history_selectedMemberId', memberId); } else { settingsBox.delete('history_selectedMemberId'); } // Naviguer vers la page historique debugPrint('MembersBoardPassages: Navigation vers /admin/history avec typeId=$typeId${memberId != null ? ', memberId=$memberId' : ' (tous les membres)'}'); if (mounted) { context.go('/admin/history'); } } /// Construit la ligne de totaux DataRow _buildTotalRow(List membres, int operationId, ThemeData theme) { final showLotType = _shouldShowLotType(); // Récupérer directement depuis les boxes Hive (déjà ouvertes) final passageBox = Hive.box(AppKeys.passagesBoxName); final allPassages = passageBox.values.where((p) => p.fkOperation == operationId).toList(); // Calculer les totaux globaux int totalCount = allPassages.length; int effectueCount = 0; double effectueMontant = 0.0; int aFinaliserCount = 0; int refuseCount = 0; int donCount = 0; int lotsCount = 0; double lotsMontant = 0.0; int videCount = 0; for (final passage in allPassages) { switch (passage.fkType) { case 1: // Effectué effectueCount++; if (passage.montant.isNotEmpty) { effectueMontant += double.tryParse(passage.montant) ?? 0.0; } break; case 2: // À finaliser aFinaliserCount++; break; case 3: // Refusé refuseCount++; break; case 4: // Don donCount++; break; case 5: // Lots if (showLotType) { lotsCount++; if (passage.montant.isNotEmpty) { lotsMontant += double.tryParse(passage.montant) ?? 0.0; } } break; case 6: // Vide videCount++; break; } } // Calculer le montant moyen global double montantMoyen = effectueCount > 0 ? effectueMontant / effectueCount : 0.0; // Compter les secteurs uniques final Set uniqueSectorIds = {}; for (final passage in allPassages) { if (passage.fkSector != null) { uniqueSectorIds.add(passage.fkSector!); } } final sectorCount = uniqueSectorIds.length; // Calculer le taux d'avancement global (passages != 2 / total passages) double tauxAvancement = 0.0; if (totalCount > 0) { final passagesNonAFinaliser = totalCount - aFinaliserCount; tauxAvancement = passagesNonAFinaliser / totalCount; } return DataRow( color: WidgetStateProperty.all(theme.colorScheme.primary.withOpacity(0.15)), cells: [ // Nom DataCell( Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(vertical: 8), child: Text( 'TOTAL', style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, fontSize: 16, ) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ), ), // Total DataCell( Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), child: Text( totalCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), // Effectués - Cliquable pour naviguer vers l'historique DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(1), // Type 1 = Effectués, tous les membres child: Container( width: double.infinity, height: double.infinity, color: Colors.green.withOpacity(0.2), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( effectueCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), Text( '(${effectueMontant.toStringAsFixed(2)}€)', style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12), ), ], ), ), ), ), ), // Montant moyen DataCell( Center( child: Text( montantMoyen > 0 ? '${montantMoyen.toStringAsFixed(2)}€' : '-', style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), // À finaliser - Cliquable pour naviguer vers l'historique DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(2), // Type 2 = À finaliser, tous les membres child: Container( width: double.infinity, height: double.infinity, color: Colors.orange.withOpacity(0.2), alignment: Alignment.center, child: Text( aFinaliserCount.toString(), style: (theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14)) .copyWith(fontWeight: FontWeight.bold, fontStyle: FontStyle.italic), ), ), ), ), ), // Refusés - Cliquable pour naviguer vers l'historique DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(3), // Type 3 = Refusés, tous les membres child: Container( width: double.infinity, height: double.infinity, color: Colors.red.withOpacity(0.2), alignment: Alignment.center, child: Text( refuseCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), ), ), // Dons - Cliquable pour naviguer vers l'historique DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(4), // Type 4 = Dons, tous les membres child: Container( width: double.infinity, height: double.infinity, color: Colors.lightBlue.withOpacity(0.2), alignment: Alignment.center, child: Text( donCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), ), ), // Lots - affiché seulement si chkLotActif = true, cliquable pour naviguer vers l'historique if (showLotType) DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(5), // Type 5 = Lots, tous les membres child: Container( width: double.infinity, height: double.infinity, color: Colors.blue.withOpacity(0.2), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( lotsCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), Text( '(${lotsMontant.toStringAsFixed(2)}€)', style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12), ), ], ), ), ), ), ), // Vides - Cliquable pour naviguer vers l'historique DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(6), // Type 6 = Vides, tous les membres child: Container( width: double.infinity, height: double.infinity, color: Colors.grey.withOpacity(0.2), alignment: Alignment.center, child: Text( videCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), ), ), // Taux d'avancement DataCell( SizedBox( width: 100, child: Row( children: [ Expanded( child: LinearProgressIndicator( value: tauxAvancement, backgroundColor: Colors.grey.shade300, valueColor: AlwaysStoppedAnimation( Colors.blue.shade600, ), ), ), const SizedBox(width: 8), Text( '${(tauxAvancement * 100).toStringAsFixed(1)}%', style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), ], ), ), ), // Secteurs DataCell( Center( child: Text( sectorCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), ], ); } /// Construit les lignes du tableau List _buildRows(List membres, int operationId, ThemeData theme) { final List rows = []; final showLotType = _shouldShowLotType(); // Récupérer directement depuis les boxes Hive (déjà ouvertes) final passageBox = Hive.box(AppKeys.passagesBoxName); final allPassages = passageBox.values.where((p) => p.fkOperation == operationId).toList(); // Récupérer tous les secteurs directement depuis la box final sectorBox = Hive.box(AppKeys.sectorsBoxName); final allSectors = sectorBox.values.toList(); for (int index = 0; index < membres.length; index++) { final membre = membres[index]; final isEvenRow = index % 2 == 0; // Récupérer les passages du membre (via ope_user_id) final memberPassages = allPassages.where((p) => p.fkUser == membre.opeUserId).toList(); // Calculer les statistiques par type int totalCount = memberPassages.length; int effectueCount = 0; double effectueMontant = 0.0; int aFinaliserCount = 0; int refuseCount = 0; int donCount = 0; int lotsCount = 0; double lotsMontant = 0.0; int videCount = 0; for (final passage in memberPassages) { switch (passage.fkType) { case 1: // Effectué effectueCount++; if (passage.montant.isNotEmpty) { effectueMontant += double.tryParse(passage.montant) ?? 0.0; } break; case 2: // À finaliser aFinaliserCount++; break; case 3: // Refusé refuseCount++; break; case 4: // Don donCount++; break; case 5: // Lots if (showLotType) { // Compter seulement si Lots est activé lotsCount++; if (passage.montant.isNotEmpty) { lotsMontant += double.tryParse(passage.montant) ?? 0.0; } } break; case 6: // Vide videCount++; break; } } // Calculer le montant moyen double montantMoyen = effectueCount > 0 ? effectueMontant / effectueCount : 0.0; // Récupérer les secteurs uniques du membre via ses passages final Set memberSectorIds = {}; for (final passage in memberPassages) { if (passage.fkSector != null) { memberSectorIds.add(passage.fkSector!); } } final sectorCount = memberSectorIds.length; final memberSectors = allSectors.where((s) => memberSectorIds.contains(s.id)).toList(); // Calculer le taux d'avancement (passages != 2 / total passages) double tauxAvancement = 0.0; if (totalCount > 0) { // Compter les passages dont le type != 2 (tous sauf "À finaliser") final passagesNonAFinaliser = totalCount - aFinaliserCount; tauxAvancement = passagesNonAFinaliser / totalCount; } rows.add( DataRow( color: WidgetStateProperty.all( isEvenRow ? Colors.white : Colors.grey.shade50, ), cells: [ // Nom - Cliquable pour naviguer vers l'historique avec le membre sélectionné DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { debugPrint('MembersBoardPassages: Clic sur membre ${membre.id} (opeUserId: ${membre.opeUserId})'); // Réinitialiser TOUS les filtres sauf le membre final settingsBox = Hive.box(AppKeys.settingsBoxName); settingsBox.delete('history_selectedSectorId'); settingsBox.delete('history_selectedSectorName'); settingsBox.delete('history_selectedTypeId'); settingsBox.delete('history_selectedPaymentTypeId'); settingsBox.delete('history_startDate'); settingsBox.delete('history_endDate'); // Sélectionner uniquement le membre (via opeUserId) settingsBox.put('history_selectedMemberId', membre.opeUserId); // Naviguer vers la page historique debugPrint('MembersBoardPassages: Navigation vers /admin/history avec opeUserId ${membre.opeUserId}'); if (mounted) { context.go('/admin/history'); } }, child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(vertical: 8), child: Text( _buildMemberDisplayName(membre), style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600) ?? const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), ), ), ), ), ), // Total - Cliquable pour naviguer vers l'historique avec le membre sélectionné DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { debugPrint('MembersBoardPassages: Clic sur Total pour membre ${membre.id} (opeUserId: ${membre.opeUserId})'); // Réinitialiser TOUS les filtres sauf le membre final settingsBox = Hive.box(AppKeys.settingsBoxName); settingsBox.delete('history_selectedSectorId'); settingsBox.delete('history_selectedSectorName'); settingsBox.delete('history_selectedTypeId'); settingsBox.delete('history_selectedPaymentTypeId'); settingsBox.delete('history_startDate'); settingsBox.delete('history_endDate'); // Sélectionner uniquement le membre (via opeUserId) settingsBox.put('history_selectedMemberId', membre.opeUserId); // Naviguer vers la page historique debugPrint('MembersBoardPassages: Navigation vers /admin/history avec opeUserId ${membre.opeUserId}'); if (mounted) { context.go('/admin/history'); } }, child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 8), child: Text( totalCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ), ), ), ), // Effectués - Cliquable pour naviguer vers l'historique avec ce membre DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(1, memberId: membre.opeUserId), // Type 1 = Effectués child: Container( width: double.infinity, height: double.infinity, color: Colors.green.withOpacity(0.1), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( effectueCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), Text( '(${effectueMontant.toStringAsFixed(2)}€)', style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12), ), ], ), ), ), ), ), // Montant moyen DataCell(Center(child: Text( montantMoyen > 0 ? '${montantMoyen.toStringAsFixed(2)}€' : '-', style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14), ))), // À finaliser - Cliquable pour naviguer vers l'historique avec ce membre DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(2, memberId: membre.opeUserId), // Type 2 = À finaliser child: Container( width: double.infinity, height: double.infinity, color: Colors.orange.withOpacity(0.1), alignment: Alignment.center, child: Text( aFinaliserCount.toString(), style: (theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14)) .copyWith(fontStyle: FontStyle.italic), ), ), ), ), ), // Refusés - Cliquable pour naviguer vers l'historique avec ce membre DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(3, memberId: membre.opeUserId), // Type 3 = Refusés child: Container( width: double.infinity, height: double.infinity, color: Colors.red.withOpacity(0.1), alignment: Alignment.center, child: Text( refuseCount.toString(), style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14), ), ), ), ), ), // Dons - Cliquable pour naviguer vers l'historique avec ce membre DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(4, memberId: membre.opeUserId), // Type 4 = Dons child: Container( width: double.infinity, height: double.infinity, color: Colors.lightBlue.withOpacity(0.1), alignment: Alignment.center, child: Text( donCount.toString(), style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14), ), ), ), ), ), // Lots - affiché seulement si chkLotActif = true, cliquable pour naviguer vers l'historique avec ce membre if (showLotType) DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(5, memberId: membre.opeUserId), // Type 5 = Lots child: Container( width: double.infinity, height: double.infinity, color: Colors.blue.withOpacity(0.1), alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( lotsCount.toString(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), Text( '(${lotsMontant.toStringAsFixed(2)}€)', style: theme.textTheme.bodySmall ?? const TextStyle(fontSize: 12), ), ], ), ), ), ), ), // Vides - Cliquable pour naviguer vers l'historique avec ce membre DataCell( MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _navigateToHistoryWithType(6, memberId: membre.opeUserId), // Type 6 = Vides child: Container( width: double.infinity, height: double.infinity, color: Colors.grey.withOpacity(0.1), alignment: Alignment.center, child: Text( videCount.toString(), style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14), ), ), ), ), ), // Taux d'avancement DataCell( SizedBox( width: 100, child: Row( children: [ Expanded( child: LinearProgressIndicator( value: tauxAvancement, backgroundColor: Colors.grey.shade300, valueColor: AlwaysStoppedAnimation( Colors.blue.shade600, ), ), ), const SizedBox(width: 8), Text( '${(tauxAvancement * 100).toStringAsFixed(1)}%', style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold) ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), ], ), ), ), // Secteurs DataCell( Row( children: [ if (sectorCount == 0) Icon( Icons.warning, color: Colors.red.shade400, size: 16, ), const SizedBox(width: 4), Text( sectorCount.toString(), style: TextStyle( fontSize: theme.textTheme.bodyMedium?.fontSize ?? 14, fontWeight: sectorCount > 0 ? FontWeight.bold : FontWeight.normal, color: sectorCount > 0 ? Colors.green.shade700 : Colors.red.shade700, ), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.map_outlined, size: 16), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { _showMemberSectorsDialog(context, membre, memberSectors.toList()); }, ), ], ), ), ], ), ); } return rows; } /// Construit le nom d'affichage d'un membre avec son sectName si disponible String _buildMemberDisplayName(MembreModel membre) { String displayName = '${membre.firstName ?? ''} ${membre.name ?? ''}'.trim(); // Ajouter le sectName entre parenthèses s'il existe if (membre.sectName != null && membre.sectName!.isNotEmpty) { displayName += ' (${membre.sectName})'; } return displayName; } /// Affiche un dialogue avec les secteurs du membre void _showMemberSectorsDialog(BuildContext context, MembreModel membre, List memberSectors) { final theme = Theme.of(context); showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('Secteurs de ${membre.firstName} ${membre.name}'), content: SizedBox( width: 400, child: memberSectors.isEmpty ? const Text('Aucun secteur attribué') : ListView.builder( shrinkWrap: true, itemCount: memberSectors.length, itemBuilder: (context, index) { final sector = memberSectors[index]; return ListTile( leading: Icon( Icons.map, color: theme.colorScheme.primary, ), title: Text(sector.libelle), subtitle: Text('Secteur #${sector.id}'), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ); }, ); } }