import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:geosector_app/presentation/widgets/charts/payment_data.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/app.dart'; import 'dart:math' as math; /// Widget de graphique en camembert pour représenter la répartition des règlements class PaymentPieChart extends StatefulWidget { /// Liste des données de règlement à afficher dans le graphique /// Si useValueListenable est true, ce paramètre est ignoré final List payments; /// 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; /// Activer l'effet 3D final bool enable3DEffect; /// Intensité de l'effet 3D (1.0 = normal, 2.0 = fort) final double effect3DIntensity; /// Activer l'effet d'explosion plus prononcé final bool enableEnhancedExplode; /// Utiliser un dégradé pour simuler l'effet 3D final bool useGradient; /// Utiliser ValueListenableBuilder pour la mise à jour automatique final bool useValueListenable; /// ID de l'utilisateur pour filtrer les passages final int? userId; const PaymentPieChart({ super.key, this.payments = const [], this.size = 300, this.labelSize = 12, this.showPercentage = true, this.showIcons = true, this.showLegend = true, this.isDonut = false, this.innerRadius = '40%', this.enable3DEffect = false, this.effect3DIntensity = 1.0, this.enableEnhancedExplode = false, this.useGradient = false, this.useValueListenable = true, this.userId, }); @override State createState() => _PaymentPieChartState(); } class _PaymentPieChartState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); _animationController.forward(); } @override void didUpdateWidget(PaymentPieChart oldWidget) { super.didUpdateWidget(oldWidget); // Relancer l'animation si les paramètres importants ont changé bool shouldResetAnimation = false; if (widget.useValueListenable != oldWidget.useValueListenable || widget.userId != oldWidget.userId) { shouldResetAnimation = true; } else if (!widget.useValueListenable) { // Pour les données statiques, comparer les éléments if (oldWidget.payments.length != widget.payments.length) { shouldResetAnimation = true; } else { for (int i = 0; i < oldWidget.payments.length; i++) { if (i >= widget.payments.length) break; if (oldWidget.payments[i].amount != widget.payments[i].amount || oldWidget.payments[i].title != widget.payments[i].title) { shouldResetAnimation = true; break; } } } } 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 paymentData = _calculatePaymentData(passagesBox); return _buildChart(paymentData); }, ); } /// Construction du widget avec des données statiques Widget _buildWithStaticData() { return _buildChart(widget.payments); } /// Calcule les données de règlement depuis la Hive box List _calculatePaymentData(Box passagesBox) { try { final passages = passagesBox.values.toList(); final currentUser = userRepository.getCurrentUser(); final int? currentUserId = widget.userId ?? currentUser?.id; // Initialiser les montants par type de règlement final Map paymentAmounts = { 0: 0.0, // Pas de règlement 1: 0.0, // Espèces 2: 0.0, // Chèques 3: 0.0, // CB }; // Parcourir les passages et calculer les montants par type de règlement for (final passage in passages) { // Vérifier si le passage appartient à l'utilisateur actuel if (currentUserId != null && passage.fkUser == currentUserId) { final int typeReglement = passage.fkTypeReglement; // Convertir la chaîne de montant en double double montant = 0.0; try { // Gérer les formats possibles (virgule ou point) String montantStr = passage.montant.replaceAll(',', '.'); montant = double.tryParse(montantStr) ?? 0.0; } catch (e) { debugPrint('Erreur de conversion du montant: ${passage.montant}'); } // Ne compter que les passages avec un montant > 0 if (montant > 0) { // Ajouter au montant total par type de règlement if (paymentAmounts.containsKey(typeReglement)) { paymentAmounts[typeReglement] = (paymentAmounts[typeReglement] ?? 0.0) + montant; } else { // Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant; } } } } // Convertir le Map en List final List paymentDataList = []; paymentAmounts.forEach((typeReglement, montant) { if (montant > 0) { // Ne retourner que les types avec un montant > 0 // Récupérer les informations depuis AppKeys.typesReglements final reglementInfo = AppKeys.typesReglements[typeReglement]; if (reglementInfo != null) { paymentDataList.add(PaymentData( typeId: typeReglement, title: reglementInfo['titre'] as String, amount: montant, color: Color(reglementInfo['couleur'] as int), icon: reglementInfo['icon_data'] as IconData, )); } else { // Fallback pour les types non définis paymentDataList.add(PaymentData( typeId: typeReglement, title: 'Type inconnu', amount: montant, color: Colors.grey, icon: Icons.help_outline, )); } } }); return paymentDataList; } catch (e) { debugPrint('Erreur lors du calcul des données de règlement: $e'); return []; } } /// Construit le graphique avec les données fournies Widget _buildChart(List paymentData) { final chartData = _prepareChartData(paymentData); // 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: Interval(0.7, 1.0, curve: Curves.elasticOut), ); final opacityAnimation = CurvedAnimation( parent: _animationController, curve: 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: (PaymentData data, _) => data.title, yValueMapper: (PaymentData data, _) => data.amount, pointColorMapper: (PaymentData data, _) { if (widget.enable3DEffect) { final index = chartData.indexOf(data); final angle = (index / chartData.length) * 2 * math.pi; return widget.useGradient ? _createEnhanced3DColor(data.color, angle) : _create3DColor( data.color, widget.effect3DIntensity); } return data.color; }, enableTooltip: true, dataLabelMapper: (PaymentData data, _) { if (widget.showPercentage) { final total = chartData.fold( 0.0, (sum, item) => sum + item.amount); final percentage = (data.amount / total * 100); return '${percentage.toStringAsFixed(1)}%'; } else { return data.title; } }, dataLabelSettings: DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition.inside, textStyle: TextStyle( fontSize: widget.labelSize, color: Colors.white, fontWeight: FontWeight.bold, ), ), innerRadius: widget.innerRadius, explode: true, explodeAll: widget.enableEnhancedExplode, explodeIndex: widget.enableEnhancedExplode ? null : 0, explodeOffset: widget.enableEnhancedExplode ? widget.enable3DEffect ? '${(12 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(8 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', opacity: widget.enable3DEffect ? 0.95 * opacityAnimation.value : opacityAnimation.value, animationDuration: 0, startAngle: 270, endAngle: 270 + (360 * progressAnimation.value).toInt(), ) : PieSeries( dataSource: chartData, xValueMapper: (PaymentData data, _) => data.title, yValueMapper: (PaymentData data, _) => data.amount, pointColorMapper: (PaymentData data, _) { if (widget.enable3DEffect) { final index = chartData.indexOf(data); final angle = (index / chartData.length) * 2 * math.pi; return widget.useGradient ? _createEnhanced3DColor(data.color, angle) : _create3DColor( data.color, widget.effect3DIntensity); } return data.color; }, enableTooltip: true, dataLabelMapper: (PaymentData data, _) { if (widget.showPercentage) { final total = chartData.fold( 0.0, (sum, item) => sum + item.amount); final percentage = (data.amount / 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, explodeAll: widget.enableEnhancedExplode, explodeIndex: widget.enableEnhancedExplode ? null : 0, explodeOffset: widget.enableEnhancedExplode ? widget.enable3DEffect ? '${(12 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(8 * explodeAnimation.value).toStringAsFixed(1)}%' : '${(5 * explodeAnimation.value).toStringAsFixed(1)}%', opacity: widget.enable3DEffect ? 0.95 * opacityAnimation.value : opacityAnimation.value, animationDuration: 0, startAngle: 270, endAngle: 270 + (360 * progressAnimation.value).toInt(), ), ], annotations: widget.showIcons ? _buildIconAnnotations(chartData) : null, palette: widget.enable3DEffect ? _create3DPalette(chartData) : null, borderWidth: widget.enable3DEffect ? 0.5 : 0, ), ); }, ); } /// Prépare les données pour le graphique en camembert List _prepareChartData(List payments) { // Filtrer les règlements avec un montant > 0 return payments.where((payment) => payment.amount > 0).toList(); } /// Crée une couleur avec effet 3D en ajustant les nuances Color _create3DColor(Color baseColor, double intensity) { final hslColor = HSLColor.fromColor(baseColor); final adjustedLightness = (hslColor.lightness + 0.15 * intensity).clamp(0.0, 1.0); final adjustedSaturation = (hslColor.saturation + 0.05 * intensity).clamp(0.0, 1.0); return hslColor .withLightness(adjustedLightness) .withSaturation(adjustedSaturation) .toColor(); } /// Crée une palette de couleurs pour l'effet 3D List _create3DPalette(List chartData) { List palette = []; for (var i = 0; i < chartData.length; i++) { var data = chartData[i]; final angle = (i / chartData.length) * 2 * math.pi; final hslColor = HSLColor.fromColor(data.color); final lightAdjustment = 0.15 * widget.effect3DIntensity * math.sin(angle); final adjustedLightness = (hslColor.lightness - 0.1 * widget.effect3DIntensity + lightAdjustment) .clamp(0.0, 1.0); final adjustedSaturation = (hslColor.saturation + 0.1 * widget.effect3DIntensity) .clamp(0.0, 1.0); final enhancedColor = hslColor .withLightness(adjustedLightness) .withSaturation(adjustedSaturation) .toColor(); palette.add(enhancedColor); } return palette; } /// Crée une couleur avec effet 3D plus avancé Color _createEnhanced3DColor(Color baseColor, double angle) { final hslColor = HSLColor.fromColor(baseColor); final adjustedLightness = hslColor.lightness + (0.2 * widget.effect3DIntensity * math.sin(angle)).clamp(-0.3, 0.3); return hslColor.withLightness(adjustedLightness.clamp(0.0, 1.0)).toColor(); } /// Crée les annotations d'icônes pour le graphique List _buildIconAnnotations( List chartData) { final List annotations = []; double total = chartData.fold(0.0, (sum, item) => sum + item.amount); double currentAngle = 0; for (int i = 0; i < chartData.length; i++) { final data = chartData[i]; final percentage = data.amount / total; final segmentAngle = percentage * 2 * 3.14159; final midAngle = currentAngle + (segmentAngle / 2); annotations.add( CircularChartAnnotation( widget: Icon( data.icon, color: Colors.white, size: 16, ), radius: '50%', angle: (midAngle * (180 / 3.14159)).toInt(), ), ); currentAngle += segmentAngle; } return annotations; } }