import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:go_router/go_router.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/constants/app_keys.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/core/services/current_user_service.dart'; // Enum pour les types de tri enum SortType { name, count, progress } enum SortOrder { none, asc, desc } class SectorDistributionCard extends StatefulWidget { final String title; final double? height; final EdgeInsetsGeometry? padding; const SectorDistributionCard({ super.key, this.title = 'Répartition par secteur', this.height, this.padding, }); @override State createState() => _SectorDistributionCardState(); } class _SectorDistributionCardState extends State { SortType? _currentSortType; SortOrder _currentSortOrder = SortOrder.none; void _onSortPressed(SortType sortType) { setState(() { if (_currentSortType == sortType) { // Cycle through: none -> asc -> desc -> none if (_currentSortOrder == SortOrder.none) { _currentSortOrder = SortOrder.asc; } else if (_currentSortOrder == SortOrder.asc) { _currentSortOrder = SortOrder.desc; } else { _currentSortOrder = SortOrder.none; _currentSortType = null; } } else { _currentSortType = sortType; _currentSortOrder = SortOrder.asc; } }); } Widget _buildSortButton(String label, SortType sortType) { final isActive = _currentSortType == sortType && _currentSortOrder != SortOrder.none; final isAsc = _currentSortType == sortType && _currentSortOrder == SortOrder.asc; final isDesc = _currentSortType == sortType && _currentSortOrder == SortOrder.desc; return InkWell( onTap: () => _onSortPressed(sortType), borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isActive ? Colors.blue.withOpacity(0.1) : Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(4), border: Border.all( color: isActive ? Colors.blue : Colors.grey[400]!, width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: TextStyle( fontSize: 12, fontWeight: isActive ? FontWeight.bold : FontWeight.normal, color: isActive ? Colors.blue : Colors.grey[700], ), ), if (isActive) ...[ const SizedBox(width: 2), Icon( isAsc ? Icons.arrow_upward : Icons.arrow_downward, size: 12, color: Colors.blue, ), ], ], ), ), ); } @override Widget build(BuildContext context) { return Container( height: widget.height, padding: widget.padding ?? const EdgeInsets.all(AppTheme.spacingM), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Ligne du titre avec boutons de tri Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( widget.title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), // Boutons de tri groupés Row( mainAxisSize: MainAxisSize.min, children: [ _buildSortButton('Nom', SortType.name), const SizedBox(width: 4), _buildSortButton('Nb', SortType.count), const SizedBox(width: 4), _buildSortButton('%', SortType.progress), ], ), ], ), const SizedBox(height: AppTheme.spacingM), Expanded( child: _buildAutoRefreshContent(), ), ], ), ); } Widget _buildAutoRefreshContent() { // Écouter les changements des deux boîtes return ValueListenableBuilder( valueListenable: Hive.box(AppKeys.sectorsBoxName).listenable(), builder: (context, Box sectorsBox, child) { return ValueListenableBuilder( valueListenable: Hive.box(AppKeys.passagesBoxName).listenable(), builder: (context, Box passagesBox, child) { return _buildContent(sectorsBox, passagesBox); }, ); }, ); } Widget _buildContent( Box sectorsBox, Box passagesBox) { try { // Calculer les statistiques final sectorStats = _calculateSectorStats(sectorsBox, passagesBox); if (sectorStats.isEmpty) { return const Center( child: Text('Aucune donnée de secteur disponible'), ); } // Appliquer le tri _applySorting(sectorStats); // Trouver le maximum de passages pour un secteur final maxCount = sectorStats.fold( 0, (max, sector) => sector['count'] > max ? sector['count'] : max, ); // Liste des secteurs directement sans sous-titre return ListView.builder( itemCount: sectorStats.length, itemBuilder: (context, index) { final sector = sectorStats[index]; return _buildSectorItem( sector['name'], sector['count'], Color(sector['color']), sectorStats, maxCount, // Passer le max pour calculer les proportions ); }, ); } catch (e) { debugPrint('Erreur lors du calcul des statistiques: $e'); return Center( child: Text('Erreur: ${e.toString()}'), ); } } List> _calculateSectorStats( Box sectorsBox, Box passagesBox, ) { // Récupérer tous les secteurs et passages final List sectors = sectorsBox.values.toList(); final List passages = passagesBox.values.toList(); // Préparer les données pour l'affichage - AFFICHER TOUS LES SECTEURS List> stats = []; for (final sector in sectors) { // Compter les passages par type pour ce secteur Map passagesByType = {}; int totalCount = 0; int passagesNotType2 = 0; // Compter tous les passages pour ce secteur for (final passage in passages) { if (passage.fkSector == sector.id) { final type = passage.fkType; passagesByType[type] = (passagesByType[type] ?? 0) + 1; totalCount++; if (type != 2) { passagesNotType2++; } } } // Calculer le pourcentage d'avancement final int progressPercentage = totalCount > 0 ? ((passagesNotType2 / totalCount) * 100).round() : 0; stats.add({ 'id': sector.id, 'name': sector.libelle, 'count': totalCount, 'passagesByType': passagesByType, 'progressPercentage': progressPercentage, 'color': sector.color.isEmpty ? 0xFF4B77BE : int.tryParse(sector.color.replaceAll('#', '0xFF')) ?? 0xFF4B77BE, }); } return stats; } void _applySorting(List> stats) { if (_currentSortType == null || _currentSortOrder == SortOrder.none) { // Tri par défaut : par nombre de passages décroissant, puis par nom stats.sort((a, b) { int countCompare = (b['count'] as int).compareTo(a['count'] as int); if (countCompare != 0) return countCompare; return (a['name'] as String).compareTo(b['name'] as String); }); return; } switch (_currentSortType!) { case SortType.name: stats.sort((a, b) { final result = (a['name'] as String).compareTo(b['name'] as String); return _currentSortOrder == SortOrder.asc ? result : -result; }); break; case SortType.count: stats.sort((a, b) { final result = (a['count'] as int).compareTo(b['count'] as int); return _currentSortOrder == SortOrder.asc ? result : -result; }); break; case SortType.progress: stats.sort((a, b) { final result = (a['progressPercentage'] as int).compareTo(b['progressPercentage'] as int); return _currentSortOrder == SortOrder.asc ? result : -result; }); break; } } Widget _buildSectorItem( String name, int count, Color color, List> allStats, int maxCount, ) { // Récupérer les données du secteur actuel final sectorData = allStats.firstWhere((s) => s['name'] == name); final Map passagesByType = sectorData['passagesByType'] ?? {}; final int progressPercentage = sectorData['progressPercentage'] ?? 0; final int sectorId = sectorData['id'] ?? 0; // Calculer le ratio par rapport au maximum (éviter division par zéro) final double widthRatio = maxCount > 0 ? count / maxCount : 0; // Style différent pour les secteurs sans passages final bool hasPassages = count > 0; final textColor = hasPassages ? Colors.black87 : Colors.grey; // Vérifier si l'utilisateur est admin final bool isAdmin = CurrentUserService.instance.canAccessAdmin; return Padding( padding: const EdgeInsets.only(bottom: AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nom du secteur et total Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: isAdmin ? InkWell( onTap: () { // Sauvegarder le secteur sélectionné et l'index de la page carte dans Hive final settingsBox = Hive.box(AppKeys.settingsBoxName); settingsBox.put('admin_selectedSectorId', sectorId); settingsBox.put('adminSelectedPageIndex', 4); // Index de la page carte // Naviguer vers le dashboard admin qui chargera la page carte context.go('/admin'); }, child: Text( name, style: TextStyle( fontSize: 14, color: textColor, fontWeight: hasPassages ? FontWeight.w600 : FontWeight.w300, decoration: TextDecoration.underline, decorationColor: textColor.withOpacity(0.5), ), overflow: TextOverflow.ellipsis, ), ) : Text( name, style: TextStyle( fontSize: 14, color: textColor, fontWeight: hasPassages ? FontWeight.w600 : FontWeight.w300, ), overflow: TextOverflow.ellipsis, ), ), Text( hasPassages ? '$count passages ($progressPercentage% d\'avancement)' : '0 passage', style: TextStyle( fontWeight: hasPassages ? FontWeight.bold : FontWeight.normal, fontSize: 13, color: textColor, ), ), ], ), const SizedBox(height: 6), // Barre horizontale cumulée avec largeur proportionnelle Align( alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: widthRatio, child: _buildStackedBar(passagesByType, count, sectorId, name), ), ), ], ), ); } Widget _buildStackedBar(Map passagesByType, int totalCount, int sectorId, String sectorName) { if (totalCount == 0) { // Barre vide pour les secteurs sans passages return Container( height: 24, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(4), ), ); } // Ordre des types : 1, 3, 4, 5, 6, 7, 8, 9, puis 2 en dernier final typeOrder = [1, 3, 4, 5, 6, 7, 8, 9, 2]; return Container( height: 24, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.grey[300]!, width: 0.5), ), child: ClipRRect( borderRadius: BorderRadius.circular(4), child: Stack( children: [ // Barre de fond Container(color: Colors.grey[100]), // Barres empilées Row( children: typeOrder.map((typeId) { final count = passagesByType[typeId] ?? 0; if (count == 0) return const SizedBox.shrink(); final percentage = (count / totalCount) * 100; final typeInfo = AppKeys.typesPassages[typeId]; final color = typeInfo != null ? Color(typeInfo['couleur2'] as int) : Colors.grey; // Vérifier si l'utilisateur est admin pour les clics final bool isAdmin = CurrentUserService.instance.canAccessAdmin; return Expanded( flex: count, child: isAdmin ? InkWell( onTap: () { // Sauvegarder les filtres dans Hive pour la page historique final settingsBox = Hive.box(AppKeys.settingsBoxName); settingsBox.put('history_selectedSectorId', sectorId); settingsBox.put('history_selectedSectorName', sectorName); settingsBox.put('history_selectedTypeId', typeId); settingsBox.put('adminSelectedPageIndex', 2); // Index de la page historique // Naviguer vers le dashboard admin qui chargera la page historique context.go('/admin'); }, child: Container( color: color, child: Center( child: percentage >= 5 // N'afficher le texte que si >= 5% ? Text( '$count (${percentage.toInt()}%)', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, shadows: [ Shadow( offset: Offset(0.5, 0.5), blurRadius: 1.0, color: Colors.black45, ), ], ), ) : null, ), ), ) : Container( color: color, child: Center( child: percentage >= 5 // N'afficher le texte que si >= 5% ? Text( '$count (${percentage.toInt()}%)', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, shadows: [ Shadow( offset: Offset(0.5, 0.5), blurRadius: 1.0, color: Colors.black45, ), ], ), ) : null, ), ), ); }).toList(), ), ], ), ), ); } }