Clean project: remove test files, debug logs, and add documentation
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
/// 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/cotisations_bloc.dart';
|
||||
import '../../bloc/cotisations_event.dart';
|
||||
import 'cotisations_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<CotisationsBloc>(
|
||||
create: (context) {
|
||||
final bloc = _getIt<CotisationsBloc>();
|
||||
// Charger les cotisations au démarrage
|
||||
bloc.add(const LoadCotisations());
|
||||
return bloc;
|
||||
},
|
||||
child: const CotisationsPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
/// Dialogue de création de cotisation
|
||||
library create_cotisation_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../bloc/cotisations_bloc.dart';
|
||||
import '../../bloc/cotisations_event.dart';
|
||||
import '../../data/models/cotisation_model.dart';
|
||||
import '../../../members/bloc/membres_bloc.dart';
|
||||
import '../../../members/bloc/membres_event.dart';
|
||||
import '../../../members/bloc/membres_state.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
|
||||
class CreateCotisationDialog extends StatefulWidget {
|
||||
const CreateCotisationDialog({super.key});
|
||||
|
||||
@override
|
||||
State<CreateCotisationDialog> createState() => _CreateCotisationDialogState();
|
||||
}
|
||||
|
||||
class _CreateCotisationDialogState extends State<CreateCotisationDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _montantController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
MembreCompletModel? _selectedMembre;
|
||||
TypeCotisation _selectedType = TypeCotisation.annuelle;
|
||||
DateTime _dateEcheance = DateTime.now().add(const Duration(days: 30));
|
||||
int _annee = DateTime.now().year;
|
||||
int? _mois;
|
||||
int? _trimestre;
|
||||
int? _semestre;
|
||||
List<MembreCompletModel> _membresDisponibles = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<MembresBloc>().add(const LoadActiveMembres());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_montantController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('Membre'),
|
||||
const SizedBox(height: 12),
|
||||
_buildMembreSelector(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Type de cotisation'),
|
||||
const SizedBox(height: 12),
|
||||
_buildTypeDropdown(),
|
||||
const SizedBox(height: 12),
|
||||
_buildPeriodeFields(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Montant'),
|
||||
const SizedBox(height: 12),
|
||||
_buildMontantField(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Échéance'),
|
||||
const SizedBox(height: 12),
|
||||
_buildDateEcheanceField(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSectionTitle('Description (optionnel)'),
|
||||
const SizedBox(height: 12),
|
||||
_buildDescriptionField(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFEF4444),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.add_card, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Créer une cotisation',
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembreSelector() {
|
||||
return BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoaded) {
|
||||
_membresDisponibles = state.membres;
|
||||
}
|
||||
|
||||
if (_selectedMembre != null) {
|
||||
return _buildSelectedMembre();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSearchField(),
|
||||
const SizedBox(height: 12),
|
||||
if (_membresDisponibles.isNotEmpty) _buildMembresList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchField() {
|
||||
return TextFormField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Rechercher un membre *',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
context.read<MembresBloc>().add(const LoadActiveMembres());
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty) {
|
||||
context.read<MembresBloc>().add(LoadMembres(recherche: value));
|
||||
} else {
|
||||
context.read<MembresBloc>().add(const LoadActiveMembres());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembresList() {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _membresDisponibles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final membre = _membresDisponibles[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(child: Text(membre.initiales)),
|
||||
title: Text(membre.nomComplet),
|
||||
subtitle: Text(membre.email),
|
||||
onTap: () => setState(() => _selectedMembre = membre),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedMembre() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green[50],
|
||||
border: Border.all(color: Colors.green[300]!),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(child: Text(_selectedMembre!.initiales)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_selectedMembre!.nomComplet,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
_selectedMembre!.email,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
onPressed: () => setState(() => _selectedMembre = null),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeDropdown() {
|
||||
return DropdownButtonFormField<TypeCotisation>(
|
||||
value: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: TypeCotisation.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(_getTypeLabel(type)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
_updatePeriodeFields();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMontantField() {
|
||||
return TextFormField(
|
||||
controller: _montantController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Montant *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
suffixText: 'XOF',
|
||||
),
|
||||
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 'Le montant doit être supérieur à 0';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeriodeFields() {
|
||||
switch (_selectedType) {
|
||||
case TypeCotisation.mensuelle:
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int>(
|
||||
value: _mois,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mois *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: List.generate(12, (index) {
|
||||
final mois = index + 1;
|
||||
return DropdownMenuItem(
|
||||
value: mois,
|
||||
child: Text(_getNomMois(mois)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) => setState(() => _mois = value),
|
||||
validator: (value) => value == null ? 'Le mois est obligatoire' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _annee.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Année *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
case TypeCotisation.trimestrielle:
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int>(
|
||||
value: _trimestre,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Trimestre *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 1, child: Text('T1 (Jan-Mar)')),
|
||||
DropdownMenuItem(value: 2, child: Text('T2 (Avr-Juin)')),
|
||||
DropdownMenuItem(value: 3, child: Text('T3 (Juil-Sep)')),
|
||||
DropdownMenuItem(value: 4, child: Text('T4 (Oct-Déc)')),
|
||||
],
|
||||
onChanged: (value) => setState(() => _trimestre = value),
|
||||
validator: (value) => value == null ? 'Le trimestre est obligatoire' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _annee.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Année *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
case TypeCotisation.semestrielle:
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int>(
|
||||
value: _semestre,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Semestre *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 1, child: Text('S1 (Jan-Juin)')),
|
||||
DropdownMenuItem(value: 2, child: Text('S2 (Juil-Déc)')),
|
||||
],
|
||||
onChanged: (value) => setState(() => _semestre = value),
|
||||
validator: (value) => value == null ? 'Le semestre est obligatoire' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _annee.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Année *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
case TypeCotisation.annuelle:
|
||||
case TypeCotisation.exceptionnelle:
|
||||
return TextFormField(
|
||||
initialValue: _annee.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Année *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) => _annee = int.tryParse(value) ?? DateTime.now().year,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDateEcheanceField() {
|
||||
return InkWell(
|
||||
onTap: () => _selectDateEcheance(context),
|
||||
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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionField() {
|
||||
return TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.notes),
|
||||
),
|
||||
maxLines: 3,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return 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(0xFFEF4444),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Créer la cotisation'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTypeLabel(TypeCotisation type) {
|
||||
switch (type) {
|
||||
case TypeCotisation.annuelle:
|
||||
return 'Annuelle';
|
||||
case TypeCotisation.mensuelle:
|
||||
return 'Mensuelle';
|
||||
case TypeCotisation.trimestrielle:
|
||||
return 'Trimestrielle';
|
||||
case TypeCotisation.semestrielle:
|
||||
return 'Semestrielle';
|
||||
case TypeCotisation.exceptionnelle:
|
||||
return 'Exceptionnelle';
|
||||
}
|
||||
}
|
||||
|
||||
String _getNomMois(int mois) {
|
||||
const moisFr = [
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
||||
];
|
||||
return (mois >= 1 && mois <= 12) ? moisFr[mois - 1] : 'Mois $mois';
|
||||
}
|
||||
|
||||
void _updatePeriodeFields() {
|
||||
_mois = null;
|
||||
_trimestre = null;
|
||||
_semestre = null;
|
||||
|
||||
final now = DateTime.now();
|
||||
switch (_selectedType) {
|
||||
case TypeCotisation.mensuelle:
|
||||
_mois = now.month;
|
||||
break;
|
||||
case TypeCotisation.trimestrielle:
|
||||
_trimestre = ((now.month - 1) ~/ 3) + 1;
|
||||
break;
|
||||
case TypeCotisation.semestrielle:
|
||||
_semestre = now.month <= 6 ? 1 : 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectDateEcheance(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateEcheance,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365 * 2)),
|
||||
);
|
||||
if (picked != null && picked != _dateEcheance) {
|
||||
setState(() => _dateEcheance = picked);
|
||||
}
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
if (_selectedMembre == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez sélectionner un membre'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final cotisation = CotisationModel(
|
||||
membreId: _selectedMembre!.id!,
|
||||
membreNom: _selectedMembre!.nom,
|
||||
membrePrenom: _selectedMembre!.prenom,
|
||||
type: _selectedType,
|
||||
montant: double.parse(_montantController.text),
|
||||
dateEcheance: _dateEcheance,
|
||||
annee: _annee,
|
||||
mois: _mois,
|
||||
trimestre: _trimestre,
|
||||
semestre: _semestre,
|
||||
description: _descriptionController.text.isNotEmpty ? _descriptionController.text : null,
|
||||
statut: StatutCotisation.nonPayee,
|
||||
);
|
||||
|
||||
context.read<CotisationsBloc>().add(CreateCotisation(cotisation: cotisation));
|
||||
Navigator.pop(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cotisation créée avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/// Dialogue de paiement de cotisation
|
||||
/// Formulaire pour enregistrer un paiement de cotisation
|
||||
library payment_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../bloc/cotisations_bloc.dart';
|
||||
import '../../bloc/cotisations_event.dart';
|
||||
import '../../data/models/cotisation_model.dart';
|
||||
|
||||
/// Dialogue de paiement de cotisation
|
||||
class PaymentDialog extends StatefulWidget {
|
||||
final CotisationModel 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();
|
||||
|
||||
MethodePaiement _selectedMethode = MethodePaiement.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<MethodePaiement>(
|
||||
value: _selectedMethode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Méthode de paiement *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.payment),
|
||||
),
|
||||
items: MethodePaiement.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(MethodePaiement methode) {
|
||||
switch (methode) {
|
||||
case MethodePaiement.waveMoney:
|
||||
return Icons.phone_android;
|
||||
case MethodePaiement.orangeMoney:
|
||||
return Icons.phone_iphone;
|
||||
case MethodePaiement.freeMoney:
|
||||
return Icons.smartphone;
|
||||
case MethodePaiement.mobileMoney:
|
||||
return Icons.mobile_friendly;
|
||||
case MethodePaiement.especes:
|
||||
return Icons.money;
|
||||
case MethodePaiement.cheque:
|
||||
return Icons.receipt_long;
|
||||
case MethodePaiement.virement:
|
||||
return Icons.account_balance;
|
||||
case MethodePaiement.carteBancaire:
|
||||
return Icons.credit_card;
|
||||
case MethodePaiement.autre:
|
||||
return Icons.more_horiz;
|
||||
}
|
||||
}
|
||||
|
||||
String _getMethodeLabel(MethodePaiement methode) {
|
||||
switch (methode) {
|
||||
case MethodePaiement.waveMoney:
|
||||
return 'Wave Money';
|
||||
case MethodePaiement.orangeMoney:
|
||||
return 'Orange Money';
|
||||
case MethodePaiement.freeMoney:
|
||||
return 'Free Money';
|
||||
case MethodePaiement.especes:
|
||||
return 'Espèces';
|
||||
case MethodePaiement.cheque:
|
||||
return 'Chèque';
|
||||
case MethodePaiement.virement:
|
||||
return 'Virement bancaire';
|
||||
case MethodePaiement.carteBancaire:
|
||||
return 'Carte bancaire';
|
||||
case MethodePaiement.mobileMoney:
|
||||
return 'Mobile Money (autre)';
|
||||
case MethodePaiement.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
|
||||
final cotisationUpdated = 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
|
||||
? StatutCotisation.payee
|
||||
: StatutCotisation.partielle,
|
||||
);
|
||||
|
||||
// Envoyer l'événement au BLoC
|
||||
context.read<CotisationsBloc>().add(EnregistrerPaiement(
|
||||
cotisationId: 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user