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; // 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( constraints: BoxConstraints( maxHeight: widget.height ?? 700, // Hauteur max, sinon s'adapte au contenu ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête de la card Container( padding: const EdgeInsets.all(AppTheme.spacingM), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 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 Expanded( child: 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 par nom membres.sort((a, b) { final nameA = '${a.firstName ?? ''} ${a.name ?? ''}'.trim(); final nameB = '${b.firstName ?? ''} ${b.name ?? ''}'.trim(); return nameA.compareTo(nameB); }); // Construire les lignes : TOTAL en première position + détails membres final allRows = [ _buildTotalRow(membres, currentOperation.id, theme), ..._buildRows(membres, currentOperation.id, theme), ]; // Utilise seulement le scroll vertical, le tableau s'adapte à la largeur return SingleChildScrollView( scrollDirection: Axis.vertical, child: SizedBox( width: double.infinity, // Prendre toute la largeur disponible 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, headingRowColor: WidgetStateProperty.all( theme.colorScheme.primary.withValues(alpha: 0.08), ), columns: _buildColumns(theme), rows: allRows, ), ), ); }, ), ), ], ), ); } /// 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: Text('Nom', style: headerStyle), ), ), // Total DataColumn( label: Expanded( child: Center( child: Text('Total', style: headerStyle), ), ), numeric: true, ), // Effectués DataColumn( label: Expanded( child: Container( color: Colors.green.withValues(alpha: 0.2), alignment: Alignment.center, child: Text('Effectués', style: headerStyle), ), ), numeric: true, ), // Montant moyen DataColumn( label: Expanded( child: Center( child: Text('Moy./passage', style: headerStyle), ), ), numeric: true, ), // À finaliser DataColumn( label: Expanded( child: Container( color: Colors.orange.withValues(alpha: 0.2), alignment: Alignment.center, child: Text('À finaliser', style: headerStyle), ), ), numeric: true, ), // Refusés DataColumn( label: Expanded( child: Container( color: Colors.red.withValues(alpha: 0.2), alignment: Alignment.center, child: Text('Refusés', style: headerStyle), ), ), numeric: true, ), // Dons DataColumn( label: Expanded( child: Container( color: Colors.lightBlue.withValues(alpha: 0.2), alignment: Alignment.center, child: Text('Dons', style: headerStyle), ), ), numeric: true, ), // Lots - affiché seulement si chkLotActif = true if (showLotType) DataColumn( label: Expanded( child: Container( color: Colors.blue.withValues(alpha: 0.2), alignment: Alignment.center, child: Text('Lots', style: headerStyle), ), ), numeric: true, ), // Vides DataColumn( label: Expanded( child: Container( color: Colors.grey.withValues(alpha: 0.2), alignment: Alignment.center, child: Text('Vides', style: headerStyle), ), ), numeric: true, ), // Taux d'avancement DataColumn( label: Expanded( child: Center( child: Text('Avancement', style: headerStyle), ), ), ), // Secteurs DataColumn( label: Expanded( child: Center( child: Text('Secteurs', style: headerStyle), ), ), numeric: true, ), ]; return columns; } /// 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 double tauxAvancement = 0.0; if (sectorCount > 0 && membres.isNotEmpty) { tauxAvancement = effectueCount / (sectorCount * membres.length); if (tauxAvancement > 1) tauxAvancement = 1.0; } return DataRow( color: WidgetStateProperty.all(theme.colorScheme.primary.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.green.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.orange.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.red.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.lightBlue.withValues(alpha: 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 if (showLotType) DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.blue.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.grey.withValues(alpha: 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).toInt()}%', 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 final memberPassages = allPassages.where((p) => p.fkUser == membre.id).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 effectués / secteurs attribués) double tauxAvancement = 0.0; bool hasWarning = false; if (sectorCount > 0) { // On considère que chaque secteur devrait avoir au moins un passage effectué tauxAvancement = effectueCount / sectorCount; if (tauxAvancement > 1) tauxAvancement = 1.0; // Limiter à 100% hasWarning = tauxAvancement < 0.5; // Avertissement si moins de 50% } else { hasWarning = true; // Avertissement si aucun secteur attribué } 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}'); // Naviguer directement vers la page history avec memberId debugPrint('MembersBoardPassages: Navigation vers /admin/history?memberId=${membre.id}'); if (mounted) { context.go('/admin/history?memberId=${membre.id}'); } }, 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 membre ${membre.id}'); // Naviguer directement vers la page history avec memberId debugPrint('MembersBoardPassages: Navigation vers /admin/history?memberId=${membre.id}'); if (mounted) { context.go('/admin/history?memberId=${membre.id}'); } }, 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.green.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.orange.withValues(alpha: 0.1), alignment: Alignment.center, child: Text( aFinaliserCount.toString(), style: (theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14)) .copyWith(fontStyle: FontStyle.italic), ), ), ), // Refusés DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.red.withValues(alpha: 0.1), alignment: Alignment.center, child: Text( refuseCount.toString(), style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14), ), ), ), // Dons DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.lightBlue.withValues(alpha: 0.1), alignment: Alignment.center, child: Text( donCount.toString(), style: theme.textTheme.bodyMedium ?? const TextStyle(fontSize: 14), ), ), ), // Lots - affiché seulement si chkLotActif = true if (showLotType) DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.blue.withValues(alpha: 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 DataCell( Container( width: double.infinity, height: double.infinity, color: Colors.grey.withValues(alpha: 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( hasWarning ? Colors.red.shade400 : Colors.green.shade400, ), ), ), const SizedBox(width: 8), if (hasWarning) Icon( Icons.warning, color: Colors.red.shade400, size: 16, ) else Text( '${(tauxAvancement * 100).toInt()}%', 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'), ), ], ); }, ); } }