Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/features/cotisations/presentation/pages/cotisations_page.dart

513 lines
16 KiB
Dart

/// 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<CotisationsPage> createState() => _CotisationsPageState();
}
class _CotisationsPageState extends State<CotisationsPage>
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<CotisationsBloc>().add(const LoadCotisations());
break;
case 1:
context.read<CotisationsBloc>().add(const LoadCotisationsPayees());
break;
case 2:
context.read<CotisationsBloc>().add(const LoadCotisationsNonPayees());
break;
case 3:
context.read<CotisationsBloc>().add(const LoadCotisationsEnRetard());
break;
}
}
@override
Widget build(BuildContext context) {
return BlocListener<CotisationsBloc, CotisationsState>(
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<CotisationsBloc, CotisationsState>(
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<Color>(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<CotisationsBloc>(),
child: PaymentDialog(cotisation: cotisation),
),
);
}
void _showCreateDialog() {
showDialog(
context: context,
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<CotisationsBloc>()),
BlocProvider.value(value: context.read<MembresBloc>()),
],
child: const CreateCotisationDialog(),
),
);
}
void _showStats() {
context.read<CotisationsBloc>().add(const LoadCotisationsStats());
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Statistiques'),
content: BlocBuilder<CotisationsBloc, CotisationsState>(
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),
),
],
),
);
}
}