/// Page de gestion des cotisations library cotisations_page; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../core/widgets/loading_widget.dart'; import '../../../../core/widgets/error_widget.dart'; import '../../bloc/cotisations_bloc.dart'; import '../../bloc/cotisations_event.dart'; import '../../bloc/cotisations_state.dart'; import '../../data/models/cotisation_model.dart'; import '../widgets/payment_dialog.dart'; import '../widgets/create_cotisation_dialog.dart'; import '../../../members/bloc/membres_bloc.dart'; /// Page principale des cotisations class CotisationsPage extends StatefulWidget { const CotisationsPage({super.key}); @override State createState() => _CotisationsPageState(); } class _CotisationsPageState 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); _loadCotisations(); } @override void dispose() { _tabController.dispose(); super.dispose(); } void _loadCotisations() { final currentTab = _tabController.index; switch (currentTab) { case 0: context.read().add(const LoadCotisations()); break; case 1: context.read().add(const LoadCotisationsPayees()); break; case 2: context.read().add(const LoadCotisationsNonPayees()); break; case 3: context.read().add(const LoadCotisationsEnRetard()); break; } } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { // Gestion des erreurs avec SnackBar if (state is CotisationsError) { 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: _loadCotisations, ), ), ); } }, child: Scaffold( appBar: AppBar( title: const Text('Cotisations'), bottom: TabBar( controller: _tabController, onTap: (_) => _loadCotisations(), 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 cotisation', ), ], ), body: TabBarView( controller: _tabController, children: [ _buildCotisationsList(), _buildCotisationsList(), _buildCotisationsList(), _buildCotisationsList(), ], ), ), ); } Widget _buildCotisationsList() { return BlocBuilder( builder: (context, state) { if (state is CotisationsLoading) { return const Center(child: AppLoadingWidget()); } if (state is CotisationsError) { return Center( child: AppErrorWidget( message: state.message, onRetry: _loadCotisations, ), ); } if (state is CotisationsLoaded) { if (state.cotisations.isEmpty) { return const Center( child: EmptyDataWidget( message: 'Aucune cotisation trouvée', icon: Icons.payment, ), ); } return RefreshIndicator( onRefresh: () async => _loadCotisations(), child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: state.cotisations.length, itemBuilder: (context, index) { final cotisation = state.cotisations[index]; return _buildCotisationCard(cotisation); }, ), ); } return const Center(child: Text('Chargez les cotisations')); }, ); } Widget _buildCotisationCard(CotisationModel cotisation) { return Card( margin: const EdgeInsets.only(bottom: 12), child: InkWell( onTap: () => _showCotisationDetails(cotisation), 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( cotisation.membreNomComplet, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( cotisation.libellePeriode, style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), ], ), ), _buildStatutChip(cotisation.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(cotisation.montant), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), if (cotisation.montantPaye != null) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Payé', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( _currencyFormat.format(cotisation.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(cotisation.dateEcheance), style: TextStyle( fontSize: 14, color: cotisation.estEnRetard ? Colors.red : null, ), ), ], ), ], ), if (cotisation.statut == StatutCotisation.partielle) Padding( padding: const EdgeInsets.only(top: 12), child: LinearProgressIndicator( value: cotisation.pourcentagePaye / 100, backgroundColor: Colors.grey[200], valueColor: const AlwaysStoppedAnimation(Colors.blue), ), ), ], ), ), ), ); } Widget _buildStatutChip(StatutCotisation statut) { Color color; String label; IconData icon; switch (statut) { case StatutCotisation.payee: color = Colors.green; label = 'Payée'; icon = Icons.check_circle; break; case StatutCotisation.nonPayee: color = Colors.orange; label = 'Non payée'; icon = Icons.pending; break; case StatutCotisation.enRetard: color = Colors.red; label = 'En retard'; icon = Icons.warning; break; case StatutCotisation.partielle: color = Colors.blue; label = 'Partielle'; icon = Icons.hourglass_bottom; break; case StatutCotisation.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 _showCotisationDetails(CotisationModel cotisation) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(cotisation.membreNomComplet), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildDetailRow('Période', cotisation.libellePeriode), _buildDetailRow('Montant', _currencyFormat.format(cotisation.montant)), if (cotisation.montantPaye != null) _buildDetailRow('Payé', _currencyFormat.format(cotisation.montantPaye)), _buildDetailRow('Restant', _currencyFormat.format(cotisation.montantRestant)), _buildDetailRow( 'Échéance', DateFormat('dd/MM/yyyy').format(cotisation.dateEcheance), ), if (cotisation.datePaiement != null) _buildDetailRow( 'Date paiement', DateFormat('dd/MM/yyyy').format(cotisation.datePaiement!), ), if (cotisation.methodePaiement != null) _buildDetailRow('Méthode', _getMethodePaiementLabel(cotisation.methodePaiement!)), ], ), ), actions: [ if (cotisation.statut != StatutCotisation.payee) TextButton.icon( onPressed: () { Navigator.pop(context); _showPaymentDialog(cotisation); }, 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(MethodePaiement methode) { switch (methode) { case MethodePaiement.especes: return 'Espèces'; case MethodePaiement.cheque: return 'Chèque'; case MethodePaiement.virement: return 'Virement'; case MethodePaiement.carteBancaire: return 'Carte bancaire'; case MethodePaiement.waveMoney: return 'Wave Money'; case MethodePaiement.orangeMoney: return 'Orange Money'; case MethodePaiement.freeMoney: return 'Free Money'; case MethodePaiement.mobileMoney: return 'Mobile Money'; case MethodePaiement.autre: return 'Autre'; } } void _showPaymentDialog(CotisationModel cotisation) { showDialog( context: context, builder: (context) => BlocProvider.value( value: context.read(), child: PaymentDialog(cotisation: cotisation), ), ); } void _showCreateDialog() { showDialog( context: context, builder: (context) => MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), BlocProvider.value(value: context.read()), ], child: const CreateCotisationDialog(), ), ); } void _showStats() { context.read().add(const LoadCotisationsStats()); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Statistiques'), content: BlocBuilder( builder: (context, state) { if (state is CotisationsStatsLoaded) { 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), ), ], ), ); } }