/// Page de gestion des contributions library contributions_page; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../shared/widgets/error_widget.dart'; import '../../../../shared/widgets/loading_widget.dart'; import '../../bloc/contributions_bloc.dart'; import '../../bloc/contributions_event.dart'; import '../../bloc/contributions_state.dart'; import '../../data/models/contribution_model.dart'; import 'package:unionflow_mobile_apps/features/contributions/presentation/widgets/create_contribution_dialog.dart'; import '../widgets/payment_dialog.dart'; import '../../../members/bloc/membres_bloc.dart'; /// Page principale des contributions class ContributionsPage extends StatefulWidget { const ContributionsPage({super.key}); @override State createState() => _ContributionsPageState(); } class _ContributionsPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final _currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: 'FCFA'); @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); _loadContributions(); } @override void dispose() { _tabController.dispose(); super.dispose(); } void _loadContributions() { final currentTab = _tabController.index; switch (currentTab) { case 0: context.read().add(const LoadContributions()); break; case 1: context.read().add(const LoadContributionsPayees()); break; case 2: context.read().add(const LoadContributionsNonPayees()); break; case 3: context.read().add(const LoadContributionsEnRetard()); break; } } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { // Gestion des erreurs avec SnackBar if (state is ContributionsError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: Colors.red, duration: const Duration(seconds: 4), action: SnackBarAction( label: 'Réessayer', textColor: Colors.white, onPressed: _loadContributions, ), ), ); } }, child: Scaffold( appBar: AppBar( title: const Text('Cotisations'), bottom: TabBar( controller: _tabController, onTap: (_) => _loadContributions(), tabs: const [ Tab(text: 'Toutes', icon: Icon(Icons.list)), Tab(text: 'Payées', icon: Icon(Icons.check_circle)), Tab(text: 'Non payées', icon: Icon(Icons.pending)), Tab(text: 'En retard', icon: Icon(Icons.warning)), ], ), actions: [ IconButton( icon: const Icon(Icons.bar_chart), onPressed: () => _showStats(), tooltip: 'Statistiques', ), IconButton( icon: const Icon(Icons.add), onPressed: () => _showCreateDialog(), tooltip: 'Nouvelle contribution', ), ], ), body: TabBarView( controller: _tabController, children: [ _buildContributionsList(), _buildContributionsList(), _buildContributionsList(), _buildContributionsList(), ], ), ), ); } Widget _buildContributionsList() { return BlocBuilder( builder: (context, state) { if (state is ContributionsLoading) { return const Center(child: AppLoadingWidget()); } if (state is ContributionsError) { return Center( child: AppErrorWidget( message: state.message, onRetry: _loadContributions, ), ); } if (state is ContributionsLoaded) { if (state.contributions.isEmpty) { return const Center( child: EmptyDataWidget( message: 'Aucune contribution trouvée', icon: Icons.payment, ), ); } return RefreshIndicator( onRefresh: () async => _loadContributions(), child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: state.contributions.length, itemBuilder: (context, index) { final contribution = state.contributions[index]; return _buildContributionCard(contribution); }, ), ); } return const Center(child: Text('Chargez les cotisations')); }, ); } Widget _buildContributionCard(ContributionModel contribution) { return Card( margin: const EdgeInsets.only(bottom: 12), child: InkWell( onTap: () => _showContributionDetails(contribution), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( contribution.membreNomComplet, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( contribution.libellePeriode, style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ), _buildStatutChip(contribution.statut), ], ), const Divider(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Montant', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( _currencyFormat.format(contribution.montant), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), if (contribution.montantPaye != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Payé', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( _currencyFormat.format(contribution.montantPaye), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( 'Échéance', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( DateFormat('dd/MM/yyyy').format(contribution.dateEcheance), style: TextStyle( fontSize: 14, color: contribution.estEnRetard ? Colors.red : null, ), ), ], ), ], ), if (contribution.statut == ContributionStatus.partielle) Padding( padding: const EdgeInsets.only(top: 12), child: LinearProgressIndicator( value: contribution.pourcentagePaye / 100, backgroundColor: Colors.grey[200], valueColor: const AlwaysStoppedAnimation(Colors.blue), ), ), ], ), ), ), ); } Widget _buildStatutChip(ContributionStatus statut) { Color color; String label; IconData icon; switch (statut) { case ContributionStatus.payee: color = Colors.green; label = 'Payée'; icon = Icons.check_circle; break; case ContributionStatus.nonPayee: color = Colors.orange; label = 'Non payée'; icon = Icons.pending; break; case ContributionStatus.enRetard: color = Colors.red; label = 'En retard'; icon = Icons.warning; break; case ContributionStatus.partielle: color = Colors.blue; label = 'Partielle'; icon = Icons.hourglass_bottom; break; case ContributionStatus.annulee: color = Colors.grey; label = 'Annulée'; icon = Icons.cancel; break; } return Chip( avatar: Icon(icon, size: 16, color: Colors.white), label: Text(label), backgroundColor: color, labelStyle: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ); } void _showContributionDetails(ContributionModel contribution) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(contribution.membreNomComplet), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildDetailRow('Période', contribution.libellePeriode), _buildDetailRow('Montant', _currencyFormat.format(contribution.montant)), if (contribution.montantPaye != null) _buildDetailRow('Payé', _currencyFormat.format(contribution.montantPaye)), _buildDetailRow('Restant', _currencyFormat.format(contribution.montantRestant)), _buildDetailRow( 'Échéance', DateFormat('dd/MM/yyyy').format(contribution.dateEcheance), ), if (contribution.datePaiement != null) _buildDetailRow( 'Date paiement', DateFormat('dd/MM/yyyy').format(contribution.datePaiement!), ), if (contribution.methodePaiement != null) _buildDetailRow('Méthode', _getMethodePaiementLabel(contribution.methodePaiement!)), ], ), ), actions: [ if (contribution.statut != ContributionStatus.payee) TextButton.icon( onPressed: () { Navigator.pop(context); _showPaymentDialog(contribution); }, icon: const Icon(Icons.payment), label: const Text('Enregistrer paiement'), ), TextButton( onPressed: () => Navigator.pop(context), child: const Text('Fermer'), ), ], ), ); } Widget _buildDetailRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( color: Colors.grey[600], fontSize: 14, ), ), Text( value, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), ); } String _getMethodePaiementLabel(PaymentMethod methode) { switch (methode) { case PaymentMethod.especes: return 'Espèces'; case PaymentMethod.cheque: return 'Chèque'; case PaymentMethod.virement: return 'Virement'; case PaymentMethod.carteBancaire: return 'Carte bancaire'; case PaymentMethod.waveMoney: return 'Wave Money'; case PaymentMethod.orangeMoney: return 'Orange Money'; case PaymentMethod.freeMoney: return 'Free Money'; case PaymentMethod.mobileMoney: return 'Mobile Money'; case PaymentMethod.autre: return 'Autre'; } } void _showPaymentDialog(ContributionModel contribution) { showDialog( context: context, builder: (context) => BlocProvider.value( value: context.read(), child: PaymentDialog(cotisation: contribution), ), ); } void _showCreateDialog() { showDialog( context: context, builder: (context) => MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), BlocProvider.value(value: context.read()), ], child: const CreateContributionDialog(), ), ); } void _showStats() { context.read().add(const LoadContributionsStats()); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Statistiques'), content: BlocBuilder( builder: (context, state) { if (state is ContributionsStatsLoaded) { return Column( mainAxisSize: MainAxisSize.min, children: [ _buildStatRow('Total', state.stats['total'].toString()), _buildStatRow('Payées', state.stats['payees'].toString()), _buildStatRow('Non payées', state.stats['nonPayees'].toString()), _buildStatRow('En retard', state.stats['enRetard'].toString()), const Divider(), _buildStatRow( 'Montant total', _currencyFormat.format(state.stats['montantTotal']), ), _buildStatRow( 'Montant payé', _currencyFormat.format(state.stats['montantPaye']), ), _buildStatRow( 'Taux recouvrement', '${state.stats['tauxRecouvrement'].toStringAsFixed(1)}%', ), ], ); } return const AppLoadingWidget(); }, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Fermer'), ), ], ), ); } Widget _buildStatRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label), Text( value, style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ); } }