import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:math' as math; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/presentation/widgets/charts/activity_chart.dart'; import 'package:geosector_app/presentation/widgets/charts/passage_pie_chart.dart'; import 'package:geosector_app/presentation/widgets/charts/payment_pie_chart.dart'; import 'package:geosector_app/presentation/widgets/charts/payment_data.dart'; import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/repositories/passage_repository.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/data/models/operation_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/shared/app_theme.dart'; /// Class pour dessiner les petits points blancs sur le fond class DotsPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.5) ..style = PaintingStyle.fill; final random = math.Random(42); // Seed fixe pour consistance final numberOfDots = (size.width * size.height) ~/ 1500; for (int i = 0; i < numberOfDots; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = 1.0 + random.nextDouble() * 2.0; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class AdminDashboardHomePage extends StatefulWidget { const AdminDashboardHomePage({Key? key}) : super(key: key); @override State createState() => _AdminDashboardHomePageState(); } class _AdminDashboardHomePageState extends State { // Données pour le tableau de bord int totalPassages = 0; double totalAmounts = 0.0; List> memberStats = []; bool isDataLoaded = false; bool isLoading = true; bool isFirstLoad = true; // Pour suivre le premier chargement // Données pour les graphiques List paymentData = []; Map passagesByType = {}; // Future pour initialiser les boîtes Hive late Future _initFuture; @override void initState() { super.initState(); // Initialiser les boîtes Hive avant de charger les données _initFuture = _initHiveBoxes().then((_) { // Charger les données une fois les boîtes initialisées _loadDashboardData(); // Après l'affichage des logs "VERIFICATION FINALE DES DONNEES", // attendre un court délai puis rafraîchir automatiquement les données Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { setState(() { isLoading = true; // Afficher le spinner pendant le rafraîchissement }); _loadDashboardData(); // Rafraîchir les données } }); }); } // Méthode pour initialiser les boîtes Hive nécessaires Future _initHiveBoxes() async { try { debugPrint('AdminDashboardHomePage: Initialisation des boîtes Hive...'); // Liste des boîtes à ouvrir final boxesToOpen = [ { 'name': AppKeys.operationsBoxName, 'type': 'OperationModel', 'opened': false }, { 'name': AppKeys.passagesBoxName, 'type': 'PassageModel', 'opened': false }, { 'name': AppKeys.sectorsBoxName, 'type': 'SectorModel', 'opened': false }, ]; // Ouvrir chaque boîte for (final boxInfo in boxesToOpen) { final boxName = boxInfo['name'] as String; if (!Hive.isBoxOpen(boxName)) { debugPrint( 'AdminDashboardHomePage: Ouverture de la boîte $boxName...'); try { switch (boxInfo['type']) { case 'OperationModel': await Hive.openBox(boxName); break; case 'PassageModel': await Hive.openBox(boxName); break; case 'SectorModel': await Hive.openBox(boxName); break; } boxInfo['opened'] = true; debugPrint( 'AdminDashboardHomePage: Boîte $boxName ouverte avec succès'); } catch (boxError) { debugPrint( 'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte $boxName: $boxError'); // Continuer malgré l'erreur } } else { boxInfo['opened'] = true; debugPrint('AdminDashboardHomePage: Boîte $boxName déjà ouverte'); } } // Vérifier si toutes les boîtes ont été ouvertes final allBoxesOpened = boxesToOpen.every((box) => box['opened'] == true); if (allBoxesOpened) { debugPrint( 'AdminDashboardHomePage: Toutes les boîtes Hive ont été ouvertes avec succès'); } else { // Identifier les boîtes qui n'ont pas pu être ouvertes final failedBoxes = boxesToOpen .where((box) => box['opened'] == false) .map((box) => box['name']) .join(', '); debugPrint( 'AdminDashboardHomePage: Certaines boîtes n\'ont pas pu être ouvertes: $failedBoxes'); } // Afficher le nombre d'éléments dans chaque boîte debugPrint('VERIFICATION FINALE DES DONNEES'); if (Hive.isBoxOpen(AppKeys.operationsBoxName)) { final operationsBox = Hive.box(AppKeys.operationsBoxName); debugPrint('Nombre d\'opérations: ${operationsBox.length}'); } if (Hive.isBoxOpen(AppKeys.passagesBoxName)) { final passagesBox = Hive.box(AppKeys.passagesBoxName); debugPrint('Nombre de passages: ${passagesBox.length}'); } if (Hive.isBoxOpen(AppKeys.sectorsBoxName)) { final sectorsBox = Hive.box(AppKeys.sectorsBoxName); debugPrint('Nombre de secteurs: ${sectorsBox.length}'); } debugPrint( 'AdminDashboardHomePage: Initialisation des boîtes Hive terminée'); } catch (e) { debugPrint( 'AdminDashboardHomePage: Erreur lors de l\'initialisation des boîtes Hive: $e'); // Ne pas propager l'erreur, mais retourner normalement // pour éviter que le FutureBuilder ne reste bloqué en état d'erreur } } /// Prépare les données pour le graphique de paiement void _preparePaymentData(List passages) { // Réinitialiser les données paymentData = []; // Compter les montants par type de règlement Map paymentAmounts = {}; // Initialiser les compteurs pour tous les types de règlement for (final typeId in AppKeys.typesReglements.keys) { paymentAmounts[typeId] = 0.0; } // Calculer les montants par type de règlement for (final passage in passages) { if (passage.fkTypeReglement != null && passage.montant != null && passage.montant.isNotEmpty) { final typeId = passage.fkTypeReglement; final amount = double.tryParse(passage.montant) ?? 0.0; paymentAmounts[typeId] = (paymentAmounts[typeId] ?? 0.0) + amount; } } // Créer les objets PaymentData paymentAmounts.forEach((typeId, amount) { if (amount > 0 && AppKeys.typesReglements.containsKey(typeId)) { final typeInfo = AppKeys.typesReglements[typeId]!; paymentData.add(PaymentData( typeId: typeId, amount: amount, title: typeInfo['titre'] as String, color: Color(typeInfo['couleur'] as int), icon: typeInfo['icon_data'] as IconData, )); } }); } Future _loadDashboardData() async { if (mounted) { setState(() { isLoading = true; }); } try { debugPrint( 'AdminDashboardHomePage: Chargement des données du tableau de bord...'); // Utiliser les instances globales définies dans app.dart // Pas besoin de Provider.of car les instances sont déjà disponibles // S'assurer que la boîte des opérations est ouverte avant d'y accéder OperationModel? currentOperation; try { // Vérifier si la boîte Hive est ouverte if (!Hive.isBoxOpen(AppKeys.operationsBoxName)) { debugPrint( 'AdminDashboardHomePage: Ouverture de la boîte operations dans _loadDashboardData...'); try { await Hive.openBox(AppKeys.operationsBoxName); debugPrint( 'AdminDashboardHomePage: Boîte operations ouverte avec succès dans _loadDashboardData'); } catch (boxError) { debugPrint( 'AdminDashboardHomePage: Erreur lors de l\'ouverture de la boîte operations dans _loadDashboardData: $boxError'); // Continuer malgré l'erreur } } // Récupérer l'opération en cours debugPrint( 'AdminDashboardHomePage: Récupération de l\'opération en cours...'); currentOperation = userRepository.getCurrentOperation(); debugPrint( 'AdminDashboardHomePage: Opération récupérée: ${currentOperation?.id ?? "null"}'); } catch (boxError) { debugPrint( 'AdminDashboardHomePage: Erreur lors de la récupération de l\'opération: $boxError'); // Afficher un message d'erreur ou gérer l'erreur de manière appropriée } if (currentOperation != null) { // Charger les passages pour l'opération en cours debugPrint( 'AdminDashboardHomePage: Chargement des passages pour l\'opération ${currentOperation.id}...'); final passages = passageRepository.getPassagesByOperation(currentOperation.id); debugPrint( 'AdminDashboardHomePage: ${passages.length} passages récupérés'); // Calculer le nombre total de passages totalPassages = passages.length; // Calculer le montant total collecté totalAmounts = passages.fold( 0.0, (sum, passage) => sum + (passage.montant != null && passage.montant.isNotEmpty ? double.tryParse(passage.montant) ?? 0.0 : 0.0)); // Préparer les données pour le graphique de paiement _preparePaymentData(passages); // Compter les passages par type passagesByType = {}; for (final passage in passages) { final typeId = passage.fkType; passagesByType[typeId] = (passagesByType[typeId] ?? 0) + 1; } // Afficher les comptages par type pour le débogage debugPrint('AdminDashboardHomePage: Comptage des passages par type:'); passagesByType.forEach((typeId, count) { final typeInfo = AppKeys.typesPassages[typeId]; final typeName = typeInfo != null ? typeInfo['titre'] : 'Inconnu'; debugPrint( 'AdminDashboardHomePage: Type $typeId ($typeName): $count passages'); }); // Charger les statistiques par membre memberStats = []; final Map memberCounts = {}; // Compter les passages par membre for (final passage in passages) { if (passage.fkUser != null) { memberCounts[passage.fkUser!] = (memberCounts[passage.fkUser!] ?? 0) + 1; } } // Récupérer les informations des membres for (final entry in memberCounts.entries) { final user = userRepository.getUserById(entry.key); if (user != null) { memberStats.add({ 'name': '${user.firstName ?? ''} ${user.name ?? ''}'.trim(), 'count': entry.value, }); } } // Trier les membres par nombre de passages (décroissant) memberStats .sort((a, b) => (b['count'] as int).compareTo(a['count'] as int)); } else { debugPrint( 'AdminDashboardHomePage: Aucune opération en cours, impossible de charger les passages'); } if (mounted) { setState(() { isDataLoaded = true; isLoading = false; isFirstLoad = false; // Marquer que le premier chargement est terminé }); } // Vérifier si les données sont correctement chargées debugPrint( 'AdminDashboardHomePage: Données chargées: isDataLoaded=$isDataLoaded, totalPassages=$totalPassages, passagesByType=${passagesByType.length} types'); } catch (e) { debugPrint( 'AdminDashboardHomePage: Erreur lors du chargement des données: $e'); if (mounted) { setState(() { isLoading = false; }); } } } @override Widget build(BuildContext context) { debugPrint('Building AdminDashboardHomePage'); return FutureBuilder( future: _initFuture, builder: (context, snapshot) { // Afficher un indicateur de chargement pendant l'initialisation des boîtes Hive if (snapshot.connectionState == ConnectionState.waiting) { debugPrint('FutureBuilder: ConnectionState.waiting'); return const Center( child: CircularProgressIndicator(), ); } // Même si nous avons une erreur, nous continuons à afficher le contenu // car nous avons modifié _initHiveBoxes pour ne pas propager les erreurs if (snapshot.hasError) { debugPrint('FutureBuilder: hasError - ${snapshot.error}'); // Nous affichons un message d'erreur mais continuons à afficher le contenu ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors de l\'initialisation: ${snapshot.error}'), backgroundColor: Colors.red, duration: const Duration(seconds: 5), action: SnackBarAction( label: 'Réessayer', onPressed: () { setState(() { _initFuture = _initHiveBoxes().then((_) { _loadDashboardData(); }); }); }, ), ), ); } else { debugPrint('FutureBuilder: Initialisation réussie'); } // L'initialisation a réussi, afficher le contenu final screenWidth = MediaQuery.of(context).size.width; final isDesktop = screenWidth > 800; // Utiliser l'instance globale définie dans app.dart // Récupérer l'opération en cours (les boîtes sont déjà ouvertes) final currentOperation = userRepository.getCurrentOperation(); // Titre dynamique avec l'ID et le nom de l'opération final String title = currentOperation != null ? 'Synthèse de l\'opération #${currentOperation.id} ${currentOperation.name}' : 'Synthèse de l\'opération'; return Stack(children: [ // Fond dégradé avec petits points blancs Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.red.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: Container(width: double.infinity, height: double.infinity), ), ), // Contenu de la page SingleChildScrollView( padding: const EdgeInsets.all(AppTheme.spacingL), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre avec bouton de rafraîchissement sur la même ligne Row( children: [ Expanded( child: Text( title, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), ), // Bouton de rafraîchissement if (!isLoading) IconButton( icon: const Icon(Icons.refresh), tooltip: 'Rafraîchir les données', onPressed: _loadDashboardData, ) else const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ], ), const SizedBox(height: AppTheme.spacingM), // Afficher un indicateur de chargement si les données ne sont pas encore chargées if (isLoading && !isDataLoaded) const Center( child: Padding( padding: EdgeInsets.all(32.0), child: CircularProgressIndicator(), ), ), // Afficher le contenu seulement si les données sont chargées ou en cours de mise à jour if (isDataLoaded || isLoading) ...[ // Cartes de synthèse isDesktop ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 2, child: _buildSummaryCard( context, 'Passages totaux', totalPassages.toString(), Icons.map_outlined, AppTheme.primaryColor, ), ), const SizedBox(width: AppTheme.spacingM), Expanded( flex: 2, child: _buildSummaryCard( context, 'Montant collecté', '${totalAmounts.toStringAsFixed(2)} €', Icons.euro_outlined, AppTheme.buttonSuccessColor, ), ), const SizedBox(width: AppTheme.spacingM), Expanded( flex: 3, child: SectorDistributionCard( key: ValueKey( 'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), height: 200, forceRefresh: !isFirstLoad, ), ), ], ) : Column( children: [ _buildSummaryCard( context, 'Passages totaux', totalPassages.toString(), Icons.map_outlined, AppTheme.primaryColor, ), const SizedBox(height: AppTheme.spacingM), _buildSummaryCard( context, 'Montant collecté', '${totalAmounts.toStringAsFixed(2)} €', Icons.euro_outlined, AppTheme.buttonSuccessColor, ), const SizedBox(height: AppTheme.spacingM), SectorDistributionCard( key: ValueKey( 'sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), height: 200, forceRefresh: !isFirstLoad, ), ], ), const SizedBox(height: AppTheme.spacingL), // Graphique d'activité Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: ActivityChart( key: ValueKey( 'activity_chart_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), height: 350, loadFromHive: true, showAllPassages: true, // Tous les passages, pas seulement ceux de l'utilisateur courant title: 'Passages réalisés par jour (15 derniers jours)', daysToShow: 15, forceRefresh: !isFirstLoad, ), // Si vous avez besoin de passer l'ID de l'opération en cours, décommentez les lignes suivantes // child: ActivityChart( // height: 350, // loadFromHive: true, // showAllPassages: true, // title: 'Passages réalisés par jour (15 derniers jours)', // daysToShow: 15, // operationId: userRepository.getCurrentOperation()?.id, // ), ), const SizedBox(height: AppTheme.spacingL), // Graphiques de répartition isDesktop ? Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _buildPassageTypeCard(context), ), const SizedBox(width: AppTheme.spacingM), Expanded( child: _buildPaymentTypeCard(context), ), ], ) : Column( children: [ _buildPassageTypeCard(context), const SizedBox(height: AppTheme.spacingM), _buildPaymentTypeCard(context), ], ), const SizedBox(height: AppTheme.spacingL), // Actions rapides - uniquement visible sur le web if (kIsWeb) ...[ Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), padding: const EdgeInsets.all(AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Actions sur cette opération', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: AppTheme.primaryColor, ), ), const SizedBox(height: AppTheme.spacingM), Wrap( spacing: AppTheme.spacingM, runSpacing: AppTheme.spacingM, children: [ _buildActionButton( context, 'Exporter les données', Icons.file_download_outlined, AppTheme.buttonPrimaryColor, () {}, ), _buildActionButton( context, 'Gérer les secteurs', Icons.map_outlined, AppTheme.accentColor, () {}, ), ], ), ], ), ), ], ], ], ), ) ]); }, ); } Widget _buildSummaryCard( BuildContext context, String label, String value, IconData icon, Color color, ) { return Container( 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: [ Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall), ), child: Icon( icon, color: color, size: 24, ), ), const SizedBox(width: AppTheme.spacingM), Text( label, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), ], ), const SizedBox(height: AppTheme.spacingM), Text( value, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 24, color: color, ), ), ], ), ); } Widget _buildChartCard( BuildContext context, String title, Widget chart, ) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(AppTheme.spacingM), child: Text( title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), ), chart, ], ), ); } // Construit la carte de répartition par type de passage avec liste Widget _buildPassageTypeCard(BuildContext context) { return Container( height: 300, // Hauteur fixe de 300px decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(AppTheme.spacingM), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Répartition par type de passage', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), Text( '$totalPassages passages', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: AppTheme.primaryColor, ), ), ], ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppTheme.spacingM), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Graphique à gauche Expanded( flex: 1, child: SizedBox( height: 180, // Taille réduite child: Builder( builder: (context) { // Vérifier si nous avons des données de passages if (passagesByType.isEmpty) { debugPrint( 'AdminDashboardHomePage: Aucune donnée de passage disponible pour le graphique'); return const Center( child: Text('Aucune donnée disponible'), ); } // Si nous avons des données, afficher le graphique // Mais d'abord, vérifier si tous les passages sont de type 2 (à finaliser) // qui est exclu par défaut dans PassagePieChart bool hasNonType2Passages = passagesByType.entries.any( (entry) => entry.key != 2 && entry.value > 0); debugPrint( 'AdminDashboardHomePage: Données pour le graphique: $passagesByType'); // Créer un widget personnalisé pour afficher le graphique ou un message // selon le contenu des données if (passagesByType.isEmpty) { debugPrint( 'AdminDashboardHomePage: Aucune donnée de passage disponible'); return const Center( child: Text('Aucune donnée disponible'), ); } // Vérifier si nous avons des données pour au moins un type int totalPassages = 0; passagesByType .forEach((_, count) => totalPassages += count); if (totalPassages == 0) { debugPrint( 'AdminDashboardHomePage: Aucun passage trouvé'); return const Center( child: Text('Aucun passage trouvé'), ); } // Vérifier si tous les passages sont de type 2 (à finaliser) if (!hasNonType2Passages) { debugPrint( 'AdminDashboardHomePage: Tous les passages sont de type 2 (à finaliser)'); // Créer un widget personnalisé pour afficher un message return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.info_outline, color: Colors.orange, size: 40, ), const SizedBox(height: 8), const Text( 'Uniquement des passages à finaliser', textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '${passagesByType[2] ?? 0} passages', textAlign: TextAlign.center, style: TextStyle( color: Colors.orange, fontWeight: FontWeight.bold, ), ), ], ); } // Sinon, afficher le graphique avec les données debugPrint( 'AdminDashboardHomePage: Affichage du graphique avec ${passagesByType.length} types'); return PassagePieChart( size: 180, passagesByType: passagesByType, loadFromHive: false, isDonut: true, innerRadius: '50%', showIcons: false, showLegend: false, ); }, ), ), ), // Liste des types à droite Expanded( flex: 1, child: Padding( padding: const EdgeInsets.only(left: AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.end, // Alignement à droite mainAxisAlignment: MainAxisAlignment.center, children: [ ...AppKeys.typesPassages.entries.map((entry) { final int typeId = entry.key; final Map typeInfo = entry.value; final int count = passagesByType[typeId] ?? 0; final Color color = Color(typeInfo['couleur2'] as int); return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment .end, // Alignement à droite children: [ Expanded( child: Text( '$count ${typeInfo['titres']}', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: color, ), textAlign: TextAlign .right, // Texte aligné à droite overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Container( width: 16, height: 16, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), ], ), ); }).toList(), ], ), ), ), ], ), ), ), ], ), ); } // Construit la carte de répartition par mode de paiement Widget _buildPaymentTypeCard(BuildContext context) { return Container( height: 300, // Hauteur fixe de 300px decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), boxShadow: AppTheme.cardShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(AppTheme.spacingM), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Répartition par mode de paiement', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), Text( '${totalAmounts.toStringAsFixed(2)} €', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: AppTheme.buttonSuccessColor, ), ), ], ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppTheme.spacingM), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Graphique à gauche Expanded( flex: 1, child: SizedBox( height: 180, // Taille réduite child: PaymentPieChart( size: 180, payments: paymentData, isDonut: true, innerRadius: '50%', showIcons: false, showLegend: false, enable3DEffect: false, // Désactiver l'effet 3D pour conserver les couleurs originales effect3DIntensity: 0.0, // Pas d'intensité 3D enableEnhancedExplode: false, // Désactiver l'explosion useGradient: false, // Ne pas utiliser de dégradé pour conserver les couleurs originales ), ), ), // Liste des types de règlement à droite Expanded( flex: 1, child: Padding( padding: const EdgeInsets.only(left: AppTheme.spacingM), child: Column( crossAxisAlignment: CrossAxisAlignment.end, // Alignement à droite mainAxisAlignment: MainAxisAlignment.center, children: [ ...[1, 2, 3].map((typeId) { // Uniquement les types 1, 2 et 3 if (!AppKeys.typesReglements.containsKey(typeId)) { return const SizedBox .shrink(); // Ignorer si le type n'existe pas } final Map typeInfo = AppKeys.typesReglements[typeId]!; // Calculer le montant total pour ce type de règlement double amount = 0.0; for (final payment in paymentData) { if (payment.typeId == typeId) { amount = payment.amount; break; } } // Ne pas afficher si le montant est 0 if (amount <= 0) { return const SizedBox.shrink(); } final Color color = Color(typeInfo['couleur'] as int); return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment .end, // Alignement à droite children: [ Expanded( child: Text( '${amount.toStringAsFixed(2)} € ${typeInfo['titre']}', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: color, ), textAlign: TextAlign .right, // Texte aligné à droite overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), Container( width: 16, height: 16, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), ], ), ); }).toList(), ], ), ), ), ], ), ), ), ], ), ); } Widget _buildActionButton( BuildContext context, String label, IconData icon, Color color, VoidCallback onPressed, ) { return ElevatedButton.icon( onPressed: onPressed, icon: Icon(icon), label: Text(label), style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingL, vertical: AppTheme.spacingM, ), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium), ), ), ); } }