Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -0,0 +1,512 @@
/// 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<ContributionsPage> createState() => _ContributionsPageState();
}
class _ContributionsPageState extends State<ContributionsPage>
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<ContributionsBloc>().add(const LoadContributions());
break;
case 1:
context.read<ContributionsBloc>().add(const LoadContributionsPayees());
break;
case 2:
context.read<ContributionsBloc>().add(const LoadContributionsNonPayees());
break;
case 3:
context.read<ContributionsBloc>().add(const LoadContributionsEnRetard());
break;
}
}
@override
Widget build(BuildContext context) {
return BlocListener<ContributionsBloc, ContributionsState>(
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<ContributionsBloc, ContributionsState>(
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<Color>(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<ContributionsBloc>(),
child: PaymentDialog(cotisation: contribution),
),
);
}
void _showCreateDialog() {
showDialog(
context: context,
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<ContributionsBloc>()),
BlocProvider.value(value: context.read<MembresBloc>()),
],
child: const CreateContributionDialog(),
),
);
}
void _showStats() {
context.read<ContributionsBloc>().add(const LoadContributionsStats());
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Statistiques'),
content: BlocBuilder<ContributionsBloc, ContributionsState>(
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),
),
],
),
);
}
}

View File

@@ -0,0 +1,30 @@
/// Wrapper BLoC pour la page des cotisations
library cotisations_page_wrapper;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../bloc/contributions_bloc.dart';
import '../../bloc/contributions_event.dart';
import 'contributions_page.dart';
final _getIt = GetIt.instance;
/// Wrapper qui fournit le BLoC à la page des cotisations
class CotisationsPageWrapper extends StatelessWidget {
const CotisationsPageWrapper({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<ContributionsBloc>(
create: (context) {
final bloc = _getIt<ContributionsBloc>();
// Charger les cotisations au démarrage
bloc.add(const LoadContributions());
return bloc;
},
child: const ContributionsPage(),
);
}
}

View File

@@ -0,0 +1,256 @@
/// Dialogue de création de contribution
library create_contribution_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../bloc/contributions_bloc.dart';
import '../../bloc/contributions_event.dart';
import '../../data/models/contribution_model.dart';
import '../../../members/bloc/membres_bloc.dart';
import '../../../members/bloc/membres_event.dart';
import '../../../members/bloc/membres_state.dart';
class CreateContributionDialog extends StatefulWidget {
const CreateContributionDialog({super.key});
@override
State<CreateContributionDialog> createState() => _CreateContributionDialogState();
}
class _CreateContributionDialogState extends State<CreateContributionDialog> {
final _formKey = GlobalKey<FormState>();
final _montantController = TextEditingController();
final _descriptionController = TextEditingController();
ContributionType _selectedType = ContributionType.mensuelle;
dynamic _selectedMembre;
DateTime _dateEcheance = DateTime.now().add(const Duration(days: 30));
bool _isLoading = false;
@override
void initState() {
super.initState();
// Charger la liste des membres
context.read<MembresBloc>().add(const LoadMembres());
}
@override
void dispose() {
_montantController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Nouvelle contribution'),
content: SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Sélection du membre
BlocBuilder<MembresBloc, MembresState>(
builder: (context, state) {
if (state is MembresLoaded) {
return DropdownButtonFormField<dynamic>(
value: _selectedMembre,
decoration: const InputDecoration(
labelText: 'Membre',
border: OutlineInputBorder(),
),
items: state.membres.map((membre) {
return DropdownMenuItem(
value: membre,
child: Text('${membre.nom} ${membre.prenom}'),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedMembre = value;
});
},
validator: (value) {
if (value == null) {
return 'Veuillez sélectionner un membre';
}
return null;
},
);
}
return const CircularProgressIndicator();
},
),
const SizedBox(height: 16),
// Type de contribution
DropdownButtonFormField<ContributionType>(
value: _selectedType,
decoration: const InputDecoration(
labelText: 'Type de contribution',
border: OutlineInputBorder(),
),
items: ContributionType.values.map((type) {
return DropdownMenuItem(
value: type,
child: Text(_getTypeLabel(type)),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedType = value;
});
}
},
),
const SizedBox(height: 16),
// Montant
TextFormField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant (FCFA)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir un montant';
}
if (double.tryParse(value) == null) {
return 'Montant invalide';
}
return null;
},
),
const SizedBox(height: 16),
// Date d'échéance
InkWell(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: _dateEcheance,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setState(() {
_dateEcheance = date;
});
}
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Date d\'échéance',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
),
child: Text(
DateFormat('dd/MM/yyyy').format(_dateEcheance),
),
),
),
const SizedBox(height: 16),
// Description
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
maxLines: 3,
),
],
),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _isLoading ? null : _createContribution,
child: _isLoading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Créer'),
),
],
);
}
String _getTypeLabel(ContributionType type) {
switch (type) {
case ContributionType.mensuelle:
return 'Mensuelle';
case ContributionType.trimestrielle:
return 'Trimestrielle';
case ContributionType.semestrielle:
return 'Semestrielle';
case ContributionType.annuelle:
return 'Annuelle';
case ContributionType.exceptionnelle:
return 'Exceptionnelle';
}
}
void _createContribution() {
if (!_formKey.currentState!.validate()) {
return;
}
if (_selectedMembre == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez sélectionner un membre'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() {
_isLoading = true;
});
final contribution = ContributionModel(
membreId: _selectedMembre!.id!,
membreNom: _selectedMembre!.nom,
membrePrenom: _selectedMembre!.prenom,
type: _selectedType,
annee: DateTime.now().year,
montant: double.parse(_montantController.text),
dateEcheance: _dateEcheance,
description: _descriptionController.text.isNotEmpty ? _descriptionController.text : null,
statut: ContributionStatus.nonPayee,
dateCreation: DateTime.now(),
dateModification: DateTime.now(),
);
context.read<ContributionsBloc>().add(CreateContribution(contribution: contribution));
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contribution créée avec succès'),
backgroundColor: Colors.green,
),
);
}
}

View File

@@ -0,0 +1,396 @@
/// Dialogue de paiement de contribution
/// Formulaire pour enregistrer un paiement de contribution
library payment_dialog;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../bloc/contributions_bloc.dart';
import '../../bloc/contributions_event.dart';
import '../../data/models/contribution_model.dart';
/// Dialogue de paiement de contribution
class PaymentDialog extends StatefulWidget {
final ContributionModel cotisation;
const PaymentDialog({
super.key,
required this.cotisation,
});
@override
State<PaymentDialog> createState() => _PaymentDialogState();
}
class _PaymentDialogState extends State<PaymentDialog> {
final _formKey = GlobalKey<FormState>();
final _montantController = TextEditingController();
final _referenceController = TextEditingController();
final _notesController = TextEditingController();
PaymentMethod _selectedMethode = PaymentMethod.waveMoney;
DateTime _datePaiement = DateTime.now();
@override
void initState() {
super.initState();
// Pré-remplir avec le montant restant
_montantController.text = widget.cotisation.montantRestant.toStringAsFixed(0);
}
@override
void dispose() {
_montantController.dispose();
_referenceController.dispose();
_notesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
constraints: const BoxConstraints(maxHeight: 500),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// En-tête
Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFF10B981),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
child: Row(
children: [
const Icon(Icons.payment, color: Colors.white),
const SizedBox(width: 12),
const Text(
'Enregistrer un paiement',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Informations de la cotisation
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[100],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.cotisation.membreNomComplet,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
widget.cotisation.libellePeriode,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Montant total:',
style: TextStyle(color: Colors.grey[600]),
),
Text(
'${NumberFormat('#,###').format(widget.cotisation.montant)} ${widget.cotisation.devise}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Déjà payé:',
style: TextStyle(color: Colors.grey[600]),
),
Text(
'${NumberFormat('#,###').format(widget.cotisation.montantPaye ?? 0)} ${widget.cotisation.devise}',
style: const TextStyle(color: Colors.green),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Restant:',
style: TextStyle(color: Colors.grey[600]),
),
Text(
'${NumberFormat('#,###').format(widget.cotisation.montantRestant)} ${widget.cotisation.devise}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
],
),
],
),
),
// Formulaire
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Montant
TextFormField(
controller: _montantController,
decoration: InputDecoration(
labelText: 'Montant à payer *',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.attach_money),
suffixText: widget.cotisation.devise,
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le montant est obligatoire';
}
final montant = double.tryParse(value);
if (montant == null || montant <= 0) {
return 'Montant invalide';
}
if (montant > widget.cotisation.montantRestant) {
return 'Montant supérieur au restant dû';
}
return null;
},
),
const SizedBox(height: 12),
// Méthode de paiement
DropdownButtonFormField<PaymentMethod>(
value: _selectedMethode,
decoration: const InputDecoration(
labelText: 'Méthode de paiement *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.payment),
),
items: PaymentMethod.values.map((methode) {
return DropdownMenuItem(
value: methode,
child: Row(
children: [
Icon(_getMethodeIcon(methode), size: 20),
const SizedBox(width: 8),
Text(_getMethodeLabel(methode)),
],
),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedMethode = value!;
});
},
),
const SizedBox(height: 12),
// Date de paiement
InkWell(
onTap: () => _selectDate(context),
child: InputDecorator(
decoration: const InputDecoration(
labelText: 'Date de paiement *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
),
child: Text(
DateFormat('dd/MM/yyyy').format(_datePaiement),
),
),
),
const SizedBox(height: 12),
// Référence
TextFormField(
controller: _referenceController,
decoration: const InputDecoration(
labelText: 'Référence de transaction',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.receipt),
hintText: 'Ex: TRX123456789',
),
),
const SizedBox(height: 12),
// Notes
TextFormField(
controller: _notesController,
decoration: const InputDecoration(
labelText: 'Notes (optionnel)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.note),
),
maxLines: 2,
),
],
),
),
),
),
// Boutons d'action
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
border: Border(top: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
),
child: const Text('Enregistrer le paiement'),
),
],
),
),
],
),
),
);
}
IconData _getMethodeIcon(PaymentMethod methode) {
switch (methode) {
case PaymentMethod.waveMoney:
return Icons.phone_android;
case PaymentMethod.orangeMoney:
return Icons.phone_iphone;
case PaymentMethod.freeMoney:
return Icons.smartphone;
case PaymentMethod.mobileMoney:
return Icons.mobile_friendly;
case PaymentMethod.especes:
return Icons.money;
case PaymentMethod.cheque:
return Icons.receipt_long;
case PaymentMethod.virement:
return Icons.account_balance;
case PaymentMethod.carteBancaire:
return Icons.credit_card;
case PaymentMethod.autre:
return Icons.more_horiz;
}
}
String _getMethodeLabel(PaymentMethod methode) {
switch (methode) {
case PaymentMethod.waveMoney:
return 'Wave Money';
case PaymentMethod.orangeMoney:
return 'Orange Money';
case PaymentMethod.freeMoney:
return 'Free Money';
case PaymentMethod.especes:
return 'Espèces';
case PaymentMethod.cheque:
return 'Chèque';
case PaymentMethod.virement:
return 'Virement bancaire';
case PaymentMethod.carteBancaire:
return 'Carte bancaire';
case PaymentMethod.mobileMoney:
return 'Mobile Money (autre)';
case PaymentMethod.autre:
return 'Autre';
}
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _datePaiement,
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (picked != null && picked != _datePaiement) {
setState(() {
_datePaiement = picked;
});
}
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
final montant = double.parse(_montantController.text);
// Créer la cotisation mise à jour
widget.cotisation.copyWith(
montantPaye: (widget.cotisation.montantPaye ?? 0) + montant,
datePaiement: _datePaiement,
methodePaiement: _selectedMethode,
referencePaiement: _referenceController.text.isNotEmpty ? _referenceController.text : null,
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
statut: (widget.cotisation.montantPaye ?? 0) + montant >= widget.cotisation.montant
? ContributionStatus.payee
: ContributionStatus.partielle,
);
// Envoyer l'événement au BLoC
context.read<ContributionsBloc>().add(RecordPayment(
contributionId: widget.cotisation.id!,
montant: montant,
methodePaiement: _selectedMethode,
datePaiement: _datePaiement,
reference: _referenceController.text.isNotEmpty ? _referenceController.text : null,
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
));
// Fermer le dialogue
Navigator.pop(context);
// Afficher un message de succès
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Paiement enregistré avec succès'),
backgroundColor: Colors.green,
),
);
}
}
}