import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:intl/intl.dart'; import '../../../../core/models/membre_model.dart'; import '../../../../core/models/cotisation_model.dart'; import '../../../../shared/theme/app_theme.dart'; /// Section des statistiques d'un membre class MembreStatsSection extends StatelessWidget { const MembreStatsSection({ super.key, required this.membre, required this.cotisations, }); final MembreModel membre; final List cotisations; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildOverviewCard(), const SizedBox(height: 16), _buildPaymentChart(), const SizedBox(height: 16), _buildStatusChart(), const SizedBox(height: 16), _buildTimelineCard(), ], ), ); } Widget _buildOverviewCard() { final totalCotisations = cotisations.length; final cotisationsPayees = cotisations.where((c) => c.statut == 'PAYEE').length; final cotisationsEnRetard = cotisations.where((c) => c.isEnRetard).length; final tauxPaiement = totalCotisations > 0 ? (cotisationsPayees / totalCotisations * 100) : 0.0; final totalMontantDu = cotisations.fold(0, (sum, c) => sum + c.montantDu); final totalMontantPaye = cotisations.fold(0, (sum, c) => sum + c.montantPaye); final membershipDuration = DateTime.now().difference(membre.dateAdhesion).inDays; return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.analytics, color: AppTheme.primaryColor, size: 24, ), const SizedBox(width: 8), const Text( 'Vue d\'ensemble', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ], ), const SizedBox(height: 20), Row( children: [ Expanded( child: _buildStatItem( 'Cotisations', '$totalCotisations', AppTheme.primaryColor, Icons.receipt_long, ), ), Expanded( child: _buildStatItem( 'Taux de paiement', '${tauxPaiement.toStringAsFixed(1)}%', tauxPaiement >= 80 ? AppTheme.successColor : tauxPaiement >= 50 ? AppTheme.warningColor : AppTheme.errorColor, Icons.trending_up, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildStatItem( 'En retard', '$cotisationsEnRetard', cotisationsEnRetard > 0 ? AppTheme.errorColor : AppTheme.successColor, Icons.warning, ), ), Expanded( child: _buildStatItem( 'Ancienneté', '${(membershipDuration / 365).floor()} an${(membershipDuration / 365).floor() > 1 ? 's' : ''}', AppTheme.infoColor, Icons.schedule, ), ), ], ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.backgroundLight, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Total payé', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), Text( _formatAmount(totalMontantPaye), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppTheme.successColor, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ const Text( 'Restant à payer', style: TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), Text( _formatAmount(totalMontantDu - totalMontantPaye), style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: totalMontantDu > totalMontantPaye ? AppTheme.warningColor : AppTheme.successColor, ), ), ], ), ], ), ), ], ), ), ); } Widget _buildStatItem(String label, String value, Color color, IconData icon) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3)), ), child: Column( children: [ Icon(icon, color: color, size: 20), const SizedBox(height: 4), Text( value, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: color, ), ), Text( label, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), textAlign: TextAlign.center, ), ], ), ); } Widget _buildPaymentChart() { if (cotisations.isEmpty) { return _buildEmptyChart('Aucune donnée de paiement'); } final paymentData = _getPaymentChartData(); return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.pie_chart, color: AppTheme.primaryColor, size: 20, ), const SizedBox(width: 8), const Text( 'Répartition des paiements', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), const SizedBox(height: 20), SizedBox( height: 200, child: PieChart( PieChartData( sections: paymentData, centerSpaceRadius: 40, sectionsSpace: 2, ), ), ), const SizedBox(height: 16), _buildChartLegend(), ], ), ), ); } Widget _buildStatusChart() { if (cotisations.isEmpty) { return _buildEmptyChart('Aucune donnée de statut'); } final statusData = _getStatusChartData(); return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.bar_chart, color: AppTheme.primaryColor, size: 20, ), const SizedBox(width: 8), const Text( 'Évolution des montants', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), const SizedBox(height: 20), SizedBox( height: 200, child: BarChart( BarChartData( barGroups: statusData, titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 60, getTitlesWidget: (value, meta) { return Text( _formatAmount(value), style: const TextStyle( fontSize: 10, color: AppTheme.textSecondary, ), ); }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < cotisations.length) { return Text( (cotisations[index].periode ?? 'N/A').substring(0, 3), style: const TextStyle( fontSize: 10, color: AppTheme.textSecondary, ), ); } return const Text(''); }, ), ), rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData(show: false), gridData: const FlGridData(show: false), ), ), ), ], ), ), ); } Widget _buildTimelineCard() { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.timeline, color: AppTheme.primaryColor, size: 20, ), const SizedBox(width: 8), const Text( 'Chronologie', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), const SizedBox(height: 16), _buildTimelineItem( 'Adhésion', DateFormat('dd/MM/yyyy').format(membre.dateAdhesion), AppTheme.primaryColor, Icons.person_add, true, ), if (cotisations.isNotEmpty) ...[ _buildTimelineItem( 'Première cotisation', DateFormat('dd/MM/yyyy').format( cotisations.map((c) => c.dateCreation).reduce((a, b) => a.isBefore(b) ? a : b), ), AppTheme.infoColor, Icons.payment, true, ), _buildTimelineItem( 'Dernière cotisation', DateFormat('dd/MM/yyyy').format( cotisations.map((c) => c.dateCreation).reduce((a, b) => a.isAfter(b) ? a : b), ), AppTheme.successColor, Icons.receipt, false, ), ], ], ), ), ); } Widget _buildTimelineItem(String title, String date, Color color, IconData icon, bool showLine) { return Row( children: [ Column( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), child: Icon(icon, color: Colors.white, size: 16), ), if (showLine) Container( width: 2, height: 24, color: color.withOpacity(0.3), ), ], ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), Text( date, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ), ), ], ); } Widget _buildEmptyChart(String message) { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(40), child: Column( children: [ Icon( Icons.bar_chart, size: 48, color: AppTheme.textHint, ), const SizedBox(height: 16), Text( message, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), ], ), ), ); } Widget _buildChartLegend() { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildLegendItem('Payé', AppTheme.successColor), _buildLegendItem('En attente', AppTheme.warningColor), _buildLegendItem('En retard', AppTheme.errorColor), ], ); } Widget _buildLegendItem(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( label, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ); } List _getPaymentChartData() { final payees = cotisations.where((c) => c.statut == 'PAYEE').length; final enAttente = cotisations.where((c) => c.statut == 'EN_ATTENTE').length; final enRetard = cotisations.where((c) => c.isEnRetard).length; final total = cotisations.length; return [ if (payees > 0) PieChartSectionData( color: AppTheme.successColor, value: payees.toDouble(), title: '${(payees / total * 100).toStringAsFixed(1)}%', radius: 50, ), if (enAttente > 0) PieChartSectionData( color: AppTheme.warningColor, value: enAttente.toDouble(), title: '${(enAttente / total * 100).toStringAsFixed(1)}%', radius: 50, ), if (enRetard > 0) PieChartSectionData( color: AppTheme.errorColor, value: enRetard.toDouble(), title: '${(enRetard / total * 100).toStringAsFixed(1)}%', radius: 50, ), ]; } List _getStatusChartData() { return cotisations.asMap().entries.map((entry) { final index = entry.key; final cotisation = entry.value; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: cotisation.montantDu, color: AppTheme.infoColor.withOpacity(0.7), width: 8, ), BarChartRodData( toY: cotisation.montantPaye, color: AppTheme.successColor, width: 8, ), ], ); }).toList(); } String _formatAmount(double amount) { return NumberFormat.currency( locale: 'fr_FR', symbol: 'FCFA', decimalDigits: 0, ).format(amount); } }