import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:flutter/material.dart'; import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/repositories/passage_repository.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart'; import 'package:geosector_app/presentation/widgets/charts/charts.dart'; class UserDashboardHomePage extends StatefulWidget { const UserDashboardHomePage({super.key}); @override State createState() => _UserDashboardHomePageState(); } class _UserDashboardHomePageState extends State { // Formater une date au format JJ/MM/YYYY String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); // Utiliser l'instance globale définie dans app.dart final size = MediaQuery.of(context).size; final isDesktop = size.width > 900; return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tableau de bord', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), Builder(builder: (context) { // Récupérer l'opération actuelle final operation = userRepository.getCurrentOperation(); if (operation != null) { return Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( '${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})', style: theme.textTheme.titleSmall?.copyWith( color: theme.colorScheme.primary.withOpacity(0.7), fontWeight: FontWeight.w500, ), ), ); } else { return const SizedBox.shrink(); } }), const SizedBox(height: 24), // Synthèse des passages _buildSummaryCards(isDesktop), const SizedBox(height: 24), // Graphique des passages _buildPassagesChart(context, theme), const SizedBox(height: 24), // Derniers passages _buildRecentPassages(context, theme), ], ), ), ), ); } // Construction des cartes de synthèse Widget _buildSummaryCards(bool isDesktop) { return Column( children: [ _buildCombinedPassagesCard(context, isDesktop), const SizedBox(height: 16), _buildCombinedPaymentsCard(isDesktop), ], ); } // Construction d'une carte combinée pour les règlements (liste + graphique) Widget _buildCombinedPaymentsCard(bool isDesktop) { // Utiliser les instances globales définies dans app.dart // Récupérer l'utilisateur actuel final currentUser = userRepository.getCurrentUser(); final int? currentUserId = currentUser?.id; // Récupérer tous les passages final passages = passageRepository.getAllPassages(); // Pas de log ici pour éviter les logs excessifs // 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 }; // Compteur pour les passages avec montant > 0 int passagesWithPaymentCount = 0; // 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) { passagesWithPaymentCount++; // 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 (0: Pas de règlement) paymentAmounts[0] = (paymentAmounts[0] ?? 0.0) + montant; // Type de règlement inconnu, ajouté à la catégorie 'Pas de règlement' } } } } // Calculer le total des règlements final double totalPayments = paymentAmounts.values.fold(0.0, (sum, amount) => sum + amount); // Convertir les montants en objets PaymentData pour le graphique final List paymentDataList = PaymentUtils.getPaymentDataFromAmounts(paymentAmounts); return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Stack( children: [ // Symbole euro en arrière-plan Positioned.fill( child: Center( child: Icon( Icons.euro_symbol, size: 180, color: Colors.blue.withOpacity(0.07), // Bleuté et estompé ), ), ), // Contenu principal Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.payments, color: AppTheme.accentColor, size: 24, ), const SizedBox(width: 8), Expanded( child: Text( 'Règlements sur $passagesWithPaymentCount passages', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), Text( '${totalPayments.toStringAsFixed(2)} €', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppTheme.accentColor, ), ), ], ), const Divider(height: 24), SizedBox( height: 250, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Liste des règlements (côté gauche) Expanded( flex: isDesktop ? 1 : 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...AppKeys.typesReglements.entries.map((entry) { final int typeId = entry.key; final Map typeData = entry.value; final double amount = paymentAmounts[typeId] ?? 0.0; final Color color = Color(typeData['couleur'] as int); return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), child: Icon( typeData['icon_data'] as IconData, color: Colors.white, size: 16, ), ), const SizedBox(width: 8), Expanded( child: Text( typeData['titre'] as String, style: const TextStyle( fontSize: 14, ), ), ), Text( '${amount.toStringAsFixed(2)} €', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: color, ), ), ], ), ); }).toList(), ], ), ), // Séparateur vertical if (isDesktop) const VerticalDivider(width: 24), // Graphique en camembert (côté droit) Expanded( flex: isDesktop ? 1 : 2, child: PaymentPieChart( payments: paymentDataList, size: double .infinity, // Utiliser tout l'espace disponible labelSize: 12, showPercentage: true, showIcons: false, // Désactiver les icônes showLegend: false, isDonut: true, innerRadius: '50%', enable3DEffect: true, // Activer l'effet 3D effect3DIntensity: 1.5, // Intensité de l'effet 3D plus forte enableEnhancedExplode: true, // Activer l'effet d'explosion amélioré useGradient: true, // Utiliser des dégradés pour renforcer l'effet 3D ), ), ], ), ), ], ), ), ], ), ); } // Construction d'une carte combinée pour les passages (liste + graphique) Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) { // Utiliser les instances globales définies dans app.dart // Récupérer l'utilisateur actuel final currentUser = userRepository.getCurrentUser(); final int? currentUserId = currentUser?.id; // Récupérer tous les passages final passages = passageRepository.getAllPassages(); // Pas de log ici pour éviter les logs excessifs // Compter les passages par type final Map passagesCounts = { 1: 0, // Effectués 2: 0, // À finaliser 3: 0, // Refusés 4: 0, // Dons 5: 0, // Lots 6: 0, // Maisons vides }; // Créer un map pour compter les types de passages final Map typesCount = {}; final Map userTypesCount = {}; // Parcourir les passages et les compter par type for (final passage in passages) { final typeId = passage.fkType; final int passageUserId = passage.fkUser; // Compter les occurrences de chaque type pour le débogage typesCount[typeId] = (typesCount[typeId] ?? 0) + 1; // Vérifier si le passage appartient à l'utilisateur actuel ou est de type 2 bool shouldCount = typeId == 2 || (currentUserId != null && passageUserId == currentUserId); if (shouldCount) { // Compter pour les statistiques de l'utilisateur userTypesCount[typeId] = (userTypesCount[typeId] ?? 0) + 1; // Ajouter au compteur des passages par type if (passagesCounts.containsKey(typeId)) { passagesCounts[typeId] = passagesCounts[typeId]! + 1; } else { // Si le type n'est pas dans notre map, l'ajouter à la catégorie par défaut (2: À finaliser) passagesCounts[2] = passagesCounts[2]! + 1; // Type de passage inconnu ajouté à 'A finaliser' } } } // Pas de log ici pour éviter les logs excessifs // Calculer le total des passages pour l'utilisateur (somme des valeurs dans userTypesCount) final int totalUserPassages = userTypesCount.values.fold(0, (sum, count) => sum + count); return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), // Réduire les paddings vertical pour donner plus d'espace au graphique child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.route, color: AppTheme.primaryColor, size: 24, ), const SizedBox(width: 8), Expanded( child: Builder(builder: (context) { // Récupérer les secteurs de l'utilisateur final userSectors = userRepository.getUserSectors(); final int sectorCount = userSectors.length; // Déterminer le titre en fonction du nombre de secteurs String title = 'Passages'; if (sectorCount > 1) { title = 'Passages sur mes $sectorCount secteurs'; } return Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ); }), ), Text( totalUserPassages.toString(), style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), ], ), const Divider(height: 24), SizedBox( height: 250, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Liste des passages (côté gauche) Expanded( flex: isDesktop ? 1 : 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ...AppKeys.typesPassages.entries.map((entry) { final int typeId = entry.key; final Map typeData = entry.value; final int count = passagesCounts[typeId] ?? 0; final Color color = Color(typeData['couleur2'] as int); // Utiliser la deuxième couleur final IconData iconData = typeData['icon_data'] as IconData; // Utiliser l'icône définie dans AppKeys return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), child: Icon( iconData, color: Colors.white, size: 16, ), ), const SizedBox(width: 8), Expanded( child: Text( typeData['titres'] as String, style: const TextStyle( fontSize: 14, ), ), ), Text( count.toString(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: color, ), ), ], ), ); }).toList(), ], ), ), // Séparateur vertical if (isDesktop) const VerticalDivider(width: 24), // Graphique en camembert (côté droit) Expanded( flex: isDesktop ? 1 : 2, child: Padding( padding: const EdgeInsets.all(8.0), child: PassagePieChart( passagesByType: passagesCounts, size: double .infinity, // Utiliser tout l'espace disponible labelSize: 12, showPercentage: true, showIcons: false, // Désactiver les icônes showLegend: false, // Désactiver la légende isDonut: true, // Activer le format donut innerRadius: '50%' // Rayon interne du donut ), ), ), ], ), ), ], ), ), ); } // Construction du graphique des passages Widget _buildPassagesChart(BuildContext context, ThemeData theme) { // Définir les types de passages à exclure // Selon la mémoire, le type 2 correspond aux passages "A finaliser" // et nous voulons les exclure du comptage pour l'utilisateur actuel final List excludePassageTypes = [2]; return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), // Réduire les paddings vertical pour donner plus d'espace child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre supprimé car déjà présent dans le widget ActivityChart SizedBox( height: 350, // Augmentation de la hauteur à 350px pour résoudre le problème de l'axe Y child: ActivityChart( // Utiliser le chargement depuis Hive directement dans le widget loadFromHive: true, // Ne pas filtrer par utilisateur (afficher tous les passages) showAllPassages: true, // Exclure les passages de type 2 (A finaliser) excludePassageTypes: excludePassageTypes, // Afficher les 15 derniers jours daysToShow: 15, periodType: 'Jour', height: 350, // Augmentation de la hauteur à 350px aussi dans le widget ), ), ], ), ), ); } // Construction de la liste des derniers passages Widget _buildRecentPassages(BuildContext context, ThemeData theme) { // Utiliser les instances globales définies dans app.dart // Récupérer tous les passages et les trier par date (les plus récents d'abord) final allPassages = passageRepository.getAllPassages(); allPassages.sort((a, b) => b.passedAt.compareTo(a.passedAt)); // Limiter aux 10 passages les plus récents final recentPassagesModels = allPassages.take(10).toList(); // Convertir les modèles de passage au format attendu par le widget PassagesListWidget final List> recentPassages = recentPassagesModels.map((passage) { // Construire l'adresse complète à partir des champs disponibles final String address = '${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}'; // Convertir le montant en double final double amount = double.tryParse(passage.montant) ?? 0.0; return { 'id': passage.id.toString(), 'address': address, 'amount': amount, 'date': passage.passedAt, 'type': passage.fkType, 'payment': passage.fkTypeReglement, 'name': passage.name, 'notes': passage.remarque, 'hasReceipt': passage.nomRecu.isNotEmpty, 'hasError': passage.emailErreur.isNotEmpty, 'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur }; }).toList(); return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Derniers passages', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), TextButton( onPressed: () { // Naviguer vers la page d'historique }, child: const Text('Voir tout'), ), ], ), ), // Utilisation du widget commun PassagesListWidget PassagesListWidget( passages: recentPassages, showFilters: false, showSearch: false, showActions: true, // Activer l'affichage des boutons d'action maxPassages: 10, // Exclure les passages de type 2 (À finaliser) excludePassageTypes: [2], // Filtrer par utilisateur courant filterByUserId: userRepository.getCurrentUser()?.id, // Période par défaut (derniers 15 jours) periodFilter: 'last15', onPassageSelected: (passage) { // Action lors de la sélection d'un passage debugPrint('Passage sélectionné: ${passage['id']}'); }, onDetailsView: (passage) { // Action lors de l'affichage des détails debugPrint('Affichage des détails: ${passage['id']}'); }, // Callback pour le bouton de modification onPassageEdit: (passage) { // Action lors de la modification d'un passage debugPrint('Modification du passage: ${passage['id']}'); // Ici, vous pourriez ouvrir un formulaire d'édition }, // Callback pour le bouton de reçu (uniquement pour les passages de type 1) onReceiptView: (passage) { // Action lors de la demande d'affichage du reçu debugPrint('Affichage du reçu pour le passage: ${passage['id']}'); // Ici, vous pourriez générer et afficher un PDF }, ), ], ), ); } }