import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show listEquals; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/services/current_user_service.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; /// Modèle de données pour le graphique en camembert des passages class PassageChartData { /// Identifiant du type de passage final int typeId; /// Nombre de passages de ce type final int count; /// Titre du type de passage final String title; /// Couleur associée au type de passage final Color color; /// Icône associée au type de passage final IconData icon; PassageChartData({ required this.typeId, required this.count, required this.title, required this.color, required this.icon, }); } /// Widget de graphique en camembert pour représenter la répartition des passages par type class PassagePieChart extends StatefulWidget { /// Liste des données de passages par type sous forme de Map avec typeId et count /// Si useValueListenable est true, ce paramètre est ignoré final Map passagesByType; /// Taille du graphique final double size; /// Taille des étiquettes final double labelSize; /// Afficher les pourcentages final bool showPercentage; /// Afficher les icônes final bool showIcons; /// Afficher la légende final bool showLegend; /// Format donut (anneau) final bool isDonut; /// Rayon central pour le format donut (en pourcentage) final String innerRadius; /// Charger les données depuis Hive (obsolète, utiliser useValueListenable) final bool loadFromHive; /// ID de l'utilisateur pour filtrer les passages final int? userId; /// Types de passages à exclure final List excludePassageTypes; /// Afficher tous les passages sans filtrer par utilisateur final bool showAllPassages; /// Utiliser ValueListenableBuilder pour la mise à jour automatique final bool useValueListenable; const PassagePieChart({ super.key, this.passagesByType = const {}, this.size = 300, this.labelSize = 12, this.showPercentage = true, this.showIcons = true, this.showLegend = true, this.isDonut = false, this.innerRadius = '40%', this.loadFromHive = false, this.userId, this.excludePassageTypes = const [2], this.showAllPassages = false, this.useValueListenable = true, }); @override State createState() => _PassagePieChartState(); } class _PassagePieChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); // Initialiser le contrôleur d'animation _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); _animationController.forward(); } @override void didUpdateWidget(PassagePieChart oldWidget) { super.didUpdateWidget(oldWidget); // Relancer l'animation si les paramètres importants ont changé final bool shouldResetAnimation = oldWidget.userId != widget.userId || !listEquals( oldWidget.excludePassageTypes, widget.excludePassageTypes) || oldWidget.showAllPassages != widget.showAllPassages || oldWidget.useValueListenable != widget.useValueListenable; if (shouldResetAnimation) { _animationController.reset(); _animationController.forward(); } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (widget.useValueListenable) { return _buildWithValueListenable(); } else { return _buildWithStaticData(); } } /// Construction du widget avec ValueListenableBuilder pour mise à jour automatique Widget _buildWithValueListenable() { return ValueListenableBuilder( valueListenable: Hive.box(AppKeys.passagesBoxName).listenable(), builder: (context, Box passagesBox, child) { final chartData = _calculatePassageData(passagesBox); return _buildChart(chartData); }, ); } /// Construction du widget avec des données statiques (ancien système) Widget _buildWithStaticData() { // Vérifier si le type Lot doit être affiché bool showLotType = true; final currentUser = CurrentUserService.instance.currentUser; if (currentUser != null && currentUser.fkEntite != null) { final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); if (userAmicale != null) { showLotType = userAmicale.chkLotActif; } } // Filtrer les données pour exclure le type 5 si nécessaire Map filteredData = Map.from(widget.passagesByType); if (!showLotType) { filteredData.remove(5); } final chartData = _prepareChartDataFromMap(filteredData); return _buildChart(chartData); } /// Calcule les données de passage depuis la Hive box List _calculatePassageData(Box passagesBox) { try { final passages = passagesBox.values.toList(); final currentUser = userRepository.getCurrentUser(); // Vérifier si le type Lot doit être affiché bool showLotType = true; if (currentUser != null && currentUser.fkEntite != null) { final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); if (userAmicale != null) { showLotType = userAmicale.chkLotActif; } } // Calculer les données selon les filtres final Map passagesByType = {}; // Initialiser tous les types de passage possibles for (final typeId in AppKeys.typesPassages.keys) { // Exclure le type Lot (5) si chkLotActif = false if (typeId == 5 && !showLotType) { continue; } if (!widget.excludePassageTypes.contains(typeId)) { passagesByType[typeId] = 0; } } // L'API filtre déjà les passages côté serveur // On compte simplement tous les passages de la box for (final passage in passages) { // Appliquer les filtres locaux uniquement bool shouldInclude = true; // Filtrer par userId si spécifié (cas particulier pour compatibilité) if (widget.userId != null) { shouldInclude = passage.fkUser == widget.userId; } // Exclure certains types if (widget.excludePassageTypes.contains(passage.fkType)) { shouldInclude = false; } // Exclure le type Lot (5) si chkLotActif = false if (passage.fkType == 5 && !showLotType) { shouldInclude = false; } if (shouldInclude) { passagesByType[passage.fkType] = (passagesByType[passage.fkType] ?? 0) + 1; } } return _prepareChartDataFromMap(passagesByType); } catch (e) { debugPrint('Erreur lors du calcul des données de passage: $e'); return []; } } /// Prépare les données pour le graphique en camembert à partir d'une Map List _prepareChartDataFromMap( Map passagesByType) { final List chartData = []; // Vérifier si le type Lot doit être affiché bool showLotType = true; final currentUser = CurrentUserService.instance.currentUser; if (currentUser != null && currentUser.fkEntite != null) { final userAmicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!); if (userAmicale != null) { showLotType = userAmicale.chkLotActif; } } // Créer les données du graphique passagesByType.forEach((typeId, count) { // Exclure le type Lot (5) si chkLotActif = false if (typeId == 5 && !showLotType) { return; // Skip ce type } // Vérifier que le type existe et que le compteur est positif if (count > 0 && AppKeys.typesPassages.containsKey(typeId)) { final typeInfo = AppKeys.typesPassages[typeId]!; chartData.add(PassageChartData( typeId: typeId, count: count, title: typeInfo['titre'] as String, color: Color(typeInfo['couleur2'] as int), icon: typeInfo['icon_data'] as IconData, )); } }); return chartData; } /// Construit le graphique avec les données fournies Widget _buildChart(List chartData) { // Si aucune donnée, afficher un message if (chartData.isEmpty) { return SizedBox( width: widget.size, height: widget.size, child: const Center( child: Text('Aucune donnée disponible'), ), ); } // Créer des animations pour différents aspects du graphique final progressAnimation = CurvedAnimation( parent: _animationController, curve: Curves.easeOutCubic, ); final explodeAnimation = CurvedAnimation( parent: _animationController, curve: const Interval(0.7, 1.0, curve: Curves.elasticOut), ); final opacityAnimation = CurvedAnimation( parent: _animationController, curve: const Interval(0.1, 0.5, curve: Curves.easeIn), ); return AnimatedBuilder( animation: _animationController, builder: (context, child) { return SizedBox( width: widget.size, height: widget.size, child: SfCircularChart( margin: EdgeInsets.zero, legend: Legend( isVisible: widget.showLegend, position: LegendPosition.bottom, overflowMode: LegendItemOverflowMode.wrap, textStyle: TextStyle(fontSize: widget.labelSize), ), tooltipBehavior: TooltipBehavior(enable: true), series: [ widget.isDonut ? DoughnutSeries( dataSource: chartData, xValueMapper: (PassageChartData data, _) => data.title, yValueMapper: (PassageChartData data, _) => data.count, pointColorMapper: (PassageChartData data, _) => data.color, enableTooltip: true, dataLabelMapper: (PassageChartData data, _) { if (widget.showPercentage) { // Calculer le pourcentage avec une décimale final total = chartData.fold( 0, (sum, item) => sum + item.count); final percentage = (data.count / total * 100); return '${percentage.toStringAsFixed(1)}%'; } else { return data.title; } }, dataLabelSettings: DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition.outside, textStyle: TextStyle(fontSize: widget.labelSize), connectorLineSettings: const ConnectorLineSettings( type: ConnectorType.curve, length: '15%', ), ), innerRadius: widget.innerRadius, explode: true, explodeIndex: 0, explodeOffset: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', opacity: opacityAnimation.value, animationDuration: 0, startAngle: 270, endAngle: 270 + (360 * progressAnimation.value).toInt(), ) : PieSeries( dataSource: chartData, xValueMapper: (PassageChartData data, _) => data.title, yValueMapper: (PassageChartData data, _) => data.count, pointColorMapper: (PassageChartData data, _) => data.color, enableTooltip: true, dataLabelMapper: (PassageChartData data, _) { if (widget.showPercentage) { // Calculer le pourcentage avec une décimale final total = chartData.fold( 0, (sum, item) => sum + item.count); final percentage = (data.count / total * 100); return '${percentage.toStringAsFixed(1)}%'; } else { return data.title; } }, dataLabelSettings: DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition.outside, textStyle: TextStyle(fontSize: widget.labelSize), connectorLineSettings: const ConnectorLineSettings( type: ConnectorType.curve, length: '15%', ), ), explode: true, explodeIndex: 0, explodeOffset: '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', opacity: opacityAnimation.value, animationDuration: 0, startAngle: 270, endAngle: 270 + (360 * progressAnimation.value).toInt(), ), ], annotations: widget.showIcons ? _buildIconAnnotations(chartData) : null, ), ); }, ); } /// Crée les annotations d'icônes pour le graphique List _buildIconAnnotations( List chartData) { final List annotations = []; // Calculer le total pour les pourcentages int total = chartData.fold(0, (sum, item) => sum + item.count); if (total == 0) return []; // Éviter la division par zéro // Position angulaire actuelle (en radians) double currentAngle = 0; for (int i = 0; i < chartData.length; i++) { final data = chartData[i]; final percentage = data.count / total; // Calculer l'angle central de ce segment final segmentAngle = percentage * 2 * 3.14159; final midAngle = currentAngle + (segmentAngle / 2); // Ajouter une annotation pour l'icône annotations.add( CircularChartAnnotation( widget: Icon( data.icon, color: Colors.white, size: 16, ), radius: '50%', angle: (midAngle * (180 / 3.14159)).toInt(), // Convertir en degrés ), ); // Mettre à jour l'angle actuel currentAngle += segmentAngle; } return annotations; } }