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/core/data/models/sector_model.dart'; import 'package:geosector_app/presentation/widgets/sector_distribution_card.dart'; import 'package:geosector_app/presentation/widgets/charts/charts.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/theme/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.withValues(alpha: 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({super.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 = {}; @override void initState() { super.initState(); _loadDashboardData(); } /// 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 // Récupérer l'opération en cours (les boxes sont déjà ouvertes par SplashPage) final currentOperation = userRepository.getCurrentOperation(); debugPrint('AdminDashboardHomePage: Opération récupérée: ${currentOperation?.id ?? "null"}'); 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.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) { 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'); final screenWidth = MediaQuery.of(context).size.width; final isDesktop = screenWidth > 800; // Récupérer l'opération en cours (les boîtes sont déjà ouvertes par SplashPage) final currentOperation = userRepository.getCurrentOperation(); // Titre dynamique avec l'ID et le nom de l'opération final String title = currentOperation != null ? 'Opération #${currentOperation.id} ${currentOperation.name}' : '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: const SizedBox(width: double.infinity, height: double.infinity), ), ), // Contenu de la page SingleChildScrollView( padding: const EdgeInsets.all(AppTheme.spacingL), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre Text( title, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), 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) ...[ // LIGNE 1 : Graphiques de répartition (type de passage et mode de paiement) 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), // LIGNE 2 : Carte de répartition par secteur (pleine largeur) ValueListenableBuilder>( valueListenable: Hive.box(AppKeys.sectorsBoxName).listenable(), builder: (context, Box box, child) { final sectorCount = box.values.length; return SectorDistributionCard( key: ValueKey('sector_distribution_${isFirstLoad ? 'initial' : 'refreshed'}_$isLoading'), title: '$sectorCount secteurs', height: 500, // Hauteur maximale pour afficher tous les secteurs ); }, ), const SizedBox(height: AppTheme.spacingL), // LIGNE 3 : 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, showAllPassages: true, // Tous les passages, pas seulement ceux de l'utilisateur courant title: 'Passages réalisés par jour (15 derniers jours)', daysToShow: 15, ), ), 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: [ const 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.primaryColor, () {}, ), _buildActionButton( context, 'Gérer les secteurs', Icons.map_outlined, AppTheme.accentColor, () {}, ), ], ), ], ), ), ], ], ], ), ), ]); } // Construit la carte de répartition par type de passage avec liste Widget _buildPassageTypeCard(BuildContext context) { return PassageSummaryCard( title: 'Passages', titleColor: AppTheme.primaryColor, titleIcon: Icons.route, height: 300, useValueListenable: false, // Utiliser les données statiques showAllPassages: true, excludePassageTypes: const [2], // Exclure "À finaliser" passagesByType: passagesByType, customTotalDisplay: (total) => '$totalPassages passages', isDesktop: MediaQuery.of(context).size.width > 800, backgroundIcon: Icons.route, backgroundIconColor: AppTheme.primaryColor, backgroundIconOpacity: 0.07, backgroundIconSize: 180, ); } // Construit la carte de répartition par mode de paiement Widget _buildPaymentTypeCard(BuildContext context) { return PaymentSummaryCard( title: 'Règlements', titleColor: AppTheme.buttonSuccessColor, titleIcon: Icons.euro, height: 300, useValueListenable: false, // Utiliser les données statiques showAllPayments: true, paymentsByType: _convertPaymentDataToMap(paymentData), customTotalDisplay: (total) => '${totalAmounts.toStringAsFixed(2)} €', isDesktop: MediaQuery.of(context).size.width > 800, backgroundIcon: Icons.euro, backgroundIconColor: AppTheme.primaryColor, backgroundIconOpacity: 0.07, backgroundIconSize: 180, ); } // Méthode helper pour convertir les PaymentData en Map Map _convertPaymentDataToMap(List paymentDataList) { final Map result = {}; for (final payment in paymentDataList) { result[payment.typeId] = payment.amount; } return result; } 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), ), ), ); } }