Clean project: remove test files, debug logs, and add documentation

This commit is contained in:
dahoud
2025-10-05 13:41:33 +00:00
parent 96a17eadbd
commit 291847924c
438 changed files with 65754 additions and 32713 deletions

View File

@@ -0,0 +1,597 @@
/// BLoC pour la gestion des cotisations
library cotisations_bloc;
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/utils/logger.dart';
import '../data/models/cotisation_model.dart';
import 'cotisations_event.dart';
import 'cotisations_state.dart';
/// BLoC pour gérer l'état des cotisations
class CotisationsBloc extends Bloc<CotisationsEvent, CotisationsState> {
CotisationsBloc() : super(const CotisationsInitial()) {
on<LoadCotisations>(_onLoadCotisations);
on<LoadCotisationById>(_onLoadCotisationById);
on<CreateCotisation>(_onCreateCotisation);
on<UpdateCotisation>(_onUpdateCotisation);
on<DeleteCotisation>(_onDeleteCotisation);
on<SearchCotisations>(_onSearchCotisations);
on<LoadCotisationsByMembre>(_onLoadCotisationsByMembre);
on<LoadCotisationsPayees>(_onLoadCotisationsPayees);
on<LoadCotisationsNonPayees>(_onLoadCotisationsNonPayees);
on<LoadCotisationsEnRetard>(_onLoadCotisationsEnRetard);
on<EnregistrerPaiement>(_onEnregistrerPaiement);
on<LoadCotisationsStats>(_onLoadCotisationsStats);
on<GenererCotisationsAnnuelles>(_onGenererCotisationsAnnuelles);
on<EnvoyerRappelPaiement>(_onEnvoyerRappelPaiement);
}
/// Charger la liste des cotisations
Future<void> _onLoadCotisations(
LoadCotisations event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'LoadCotisations', data: {
'page': event.page,
'size': event.size,
});
emit(const CotisationsLoading(message: 'Chargement des cotisations...'));
// Simuler un délai réseau
await Future.delayed(const Duration(milliseconds: 500));
// Données mock
final cotisations = _getMockCotisations();
final total = cotisations.length;
final totalPages = (total / event.size).ceil();
// Pagination
final start = event.page * event.size;
final end = (start + event.size).clamp(0, total);
final paginatedCotisations = cotisations.sublist(
start.clamp(0, total),
end,
);
emit(CotisationsLoaded(
cotisations: paginatedCotisations,
total: total,
page: event.page,
size: event.size,
totalPages: totalPages,
));
AppLogger.blocState('CotisationsBloc', 'CotisationsLoaded', data: {
'count': paginatedCotisations.length,
'total': total,
});
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors du chargement des cotisations',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Erreur lors du chargement des cotisations',
error: e,
));
}
}
/// Charger une cotisation par ID
Future<void> _onLoadCotisationById(
LoadCotisationById event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'LoadCotisationById', data: {
'id': event.id,
});
emit(const CotisationsLoading(message: 'Chargement de la cotisation...'));
await Future.delayed(const Duration(milliseconds: 300));
final cotisations = _getMockCotisations();
final cotisation = cotisations.firstWhere(
(c) => c.id == event.id,
orElse: () => throw Exception('Cotisation non trouvée'),
);
emit(CotisationDetailLoaded(cotisation: cotisation));
AppLogger.blocState('CotisationsBloc', 'CotisationDetailLoaded');
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors du chargement de la cotisation',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Cotisation non trouvée',
error: e,
));
}
}
/// Créer une nouvelle cotisation
Future<void> _onCreateCotisation(
CreateCotisation event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'CreateCotisation');
emit(const CotisationsLoading(message: 'Création de la cotisation...'));
await Future.delayed(const Duration(milliseconds: 500));
final newCotisation = event.cotisation.copyWith(
id: 'cot_${DateTime.now().millisecondsSinceEpoch}',
dateCreation: DateTime.now(),
);
emit(CotisationCreated(cotisation: newCotisation));
AppLogger.blocState('CotisationsBloc', 'CotisationCreated');
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors de la création de la cotisation',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Erreur lors de la création de la cotisation',
error: e,
));
}
}
/// Mettre à jour une cotisation
Future<void> _onUpdateCotisation(
UpdateCotisation event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'UpdateCotisation', data: {
'id': event.id,
});
emit(const CotisationsLoading(message: 'Mise à jour de la cotisation...'));
await Future.delayed(const Duration(milliseconds: 500));
final updatedCotisation = event.cotisation.copyWith(
id: event.id,
dateModification: DateTime.now(),
);
emit(CotisationUpdated(cotisation: updatedCotisation));
AppLogger.blocState('CotisationsBloc', 'CotisationUpdated');
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors de la mise à jour de la cotisation',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Erreur lors de la mise à jour de la cotisation',
error: e,
));
}
}
/// Supprimer une cotisation
Future<void> _onDeleteCotisation(
DeleteCotisation event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'DeleteCotisation', data: {
'id': event.id,
});
emit(const CotisationsLoading(message: 'Suppression de la cotisation...'));
await Future.delayed(const Duration(milliseconds: 500));
emit(CotisationDeleted(id: event.id));
AppLogger.blocState('CotisationsBloc', 'CotisationDeleted');
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors de la suppression de la cotisation',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Erreur lors de la suppression de la cotisation',
error: e,
));
}
}
/// Rechercher des cotisations
Future<void> _onSearchCotisations(
SearchCotisations event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'SearchCotisations');
emit(const CotisationsLoading(message: 'Recherche en cours...'));
await Future.delayed(const Duration(milliseconds: 500));
var cotisations = _getMockCotisations();
// Filtrer par membre
if (event.membreId != null) {
cotisations = cotisations
.where((c) => c.membreId == event.membreId)
.toList();
}
// Filtrer par statut
if (event.statut != null) {
cotisations = cotisations
.where((c) => c.statut == event.statut)
.toList();
}
// Filtrer par type
if (event.type != null) {
cotisations = cotisations
.where((c) => c.type == event.type)
.toList();
}
// Filtrer par année
if (event.annee != null) {
cotisations = cotisations
.where((c) => c.annee == event.annee)
.toList();
}
final total = cotisations.length;
final totalPages = (total / event.size).ceil();
// Pagination
final start = event.page * event.size;
final end = (start + event.size).clamp(0, total);
final paginatedCotisations = cotisations.sublist(
start.clamp(0, total),
end,
);
emit(CotisationsLoaded(
cotisations: paginatedCotisations,
total: total,
page: event.page,
size: event.size,
totalPages: totalPages,
));
AppLogger.blocState('CotisationsBloc', 'CotisationsLoaded (search)');
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors de la recherche de cotisations',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Erreur lors de la recherche',
error: e,
));
}
}
/// Charger les cotisations d'un membre
Future<void> _onLoadCotisationsByMembre(
LoadCotisationsByMembre event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'LoadCotisationsByMembre', data: {
'membreId': event.membreId,
});
emit(const CotisationsLoading(message: 'Chargement des cotisations du membre...'));
await Future.delayed(const Duration(milliseconds: 500));
final cotisations = _getMockCotisations()
.where((c) => c.membreId == event.membreId)
.toList();
final total = cotisations.length;
final totalPages = (total / event.size).ceil();
emit(CotisationsLoaded(
cotisations: cotisations,
total: total,
page: event.page,
size: event.size,
totalPages: totalPages,
));
AppLogger.blocState('CotisationsBloc', 'CotisationsLoaded (by membre)');
} catch (e, stackTrace) {
AppLogger.error(
'Erreur lors du chargement des cotisations du membre',
error: e,
stackTrace: stackTrace,
);
emit(CotisationsError(
message: 'Erreur lors du chargement',
error: e,
));
}
}
/// Charger les cotisations payées
Future<void> _onLoadCotisationsPayees(
LoadCotisationsPayees event,
Emitter<CotisationsState> emit,
) async {
try {
emit(const CotisationsLoading(message: 'Chargement des cotisations payées...'));
await Future.delayed(const Duration(milliseconds: 500));
final cotisations = _getMockCotisations()
.where((c) => c.statut == StatutCotisation.payee)
.toList();
final total = cotisations.length;
final totalPages = (total / event.size).ceil();
emit(CotisationsLoaded(
cotisations: cotisations,
total: total,
page: event.page,
size: event.size,
totalPages: totalPages,
));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur', error: e));
}
}
/// Charger les cotisations non payées
Future<void> _onLoadCotisationsNonPayees(
LoadCotisationsNonPayees event,
Emitter<CotisationsState> emit,
) async {
try {
emit(const CotisationsLoading(message: 'Chargement des cotisations non payées...'));
await Future.delayed(const Duration(milliseconds: 500));
final cotisations = _getMockCotisations()
.where((c) => c.statut == StatutCotisation.nonPayee)
.toList();
final total = cotisations.length;
final totalPages = (total / event.size).ceil();
emit(CotisationsLoaded(
cotisations: cotisations,
total: total,
page: event.page,
size: event.size,
totalPages: totalPages,
));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur', error: e));
}
}
/// Charger les cotisations en retard
Future<void> _onLoadCotisationsEnRetard(
LoadCotisationsEnRetard event,
Emitter<CotisationsState> emit,
) async {
try {
emit(const CotisationsLoading(message: 'Chargement des cotisations en retard...'));
await Future.delayed(const Duration(milliseconds: 500));
final cotisations = _getMockCotisations()
.where((c) => c.statut == StatutCotisation.enRetard)
.toList();
final total = cotisations.length;
final totalPages = (total / event.size).ceil();
emit(CotisationsLoaded(
cotisations: cotisations,
total: total,
page: event.page,
size: event.size,
totalPages: totalPages,
));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur', error: e));
}
}
/// Enregistrer un paiement
Future<void> _onEnregistrerPaiement(
EnregistrerPaiement event,
Emitter<CotisationsState> emit,
) async {
try {
AppLogger.blocEvent('CotisationsBloc', 'EnregistrerPaiement');
emit(const CotisationsLoading(message: 'Enregistrement du paiement...'));
await Future.delayed(const Duration(milliseconds: 500));
final cotisations = _getMockCotisations();
final cotisation = cotisations.firstWhere((c) => c.id == event.cotisationId);
final updatedCotisation = cotisation.copyWith(
montantPaye: event.montant,
datePaiement: event.datePaiement,
methodePaiement: event.methodePaiement,
numeroPaiement: event.numeroPaiement,
referencePaiement: event.referencePaiement,
statut: event.montant >= cotisation.montant
? StatutCotisation.payee
: StatutCotisation.partielle,
dateModification: DateTime.now(),
);
emit(PaiementEnregistre(cotisation: updatedCotisation));
AppLogger.blocState('CotisationsBloc', 'PaiementEnregistre');
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur lors de l\'enregistrement du paiement', error: e));
}
}
/// Charger les statistiques
Future<void> _onLoadCotisationsStats(
LoadCotisationsStats event,
Emitter<CotisationsState> emit,
) async {
try {
emit(const CotisationsLoading(message: 'Chargement des statistiques...'));
await Future.delayed(const Duration(milliseconds: 500));
final cotisations = _getMockCotisations();
final stats = {
'total': cotisations.length,
'payees': cotisations.where((c) => c.statut == StatutCotisation.payee).length,
'nonPayees': cotisations.where((c) => c.statut == StatutCotisation.nonPayee).length,
'enRetard': cotisations.where((c) => c.statut == StatutCotisation.enRetard).length,
'partielles': cotisations.where((c) => c.statut == StatutCotisation.partielle).length,
'montantTotal': cotisations.fold<double>(0, (sum, c) => sum + c.montant),
'montantPaye': cotisations.fold<double>(0, (sum, c) => sum + (c.montantPaye ?? 0)),
'montantRestant': cotisations.fold<double>(0, (sum, c) => sum + c.montantRestant),
'tauxRecouvrement': 0.0,
};
if (stats['montantTotal']! > 0) {
stats['tauxRecouvrement'] = (stats['montantPaye']! / stats['montantTotal']!) * 100;
}
emit(CotisationsStatsLoaded(stats: stats));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur', error: e));
}
}
/// Générer les cotisations annuelles
Future<void> _onGenererCotisationsAnnuelles(
GenererCotisationsAnnuelles event,
Emitter<CotisationsState> emit,
) async {
try {
emit(const CotisationsLoading(message: 'Génération des cotisations...'));
await Future.delayed(const Duration(seconds: 1));
// Simuler la génération de 50 cotisations
emit(const CotisationsGenerees(nombreGenere: 50));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur', error: e));
}
}
/// Envoyer un rappel de paiement
Future<void> _onEnvoyerRappelPaiement(
EnvoyerRappelPaiement event,
Emitter<CotisationsState> emit,
) async {
try {
emit(const CotisationsLoading(message: 'Envoi du rappel...'));
await Future.delayed(const Duration(milliseconds: 500));
emit(RappelEnvoye(cotisationId: event.cotisationId));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
emit(CotisationsError(message: 'Erreur', error: e));
}
}
/// Données mock pour les tests
List<CotisationModel> _getMockCotisations() {
final now = DateTime.now();
return [
CotisationModel(
id: 'cot_001',
membreId: 'mbr_001',
membreNom: 'Dupont',
membrePrenom: 'Jean',
montant: 50000,
dateEcheance: DateTime(now.year, 12, 31),
annee: now.year,
statut: StatutCotisation.payee,
montantPaye: 50000,
datePaiement: DateTime(now.year, 1, 15),
methodePaiement: MethodePaiement.virement,
),
CotisationModel(
id: 'cot_002',
membreId: 'mbr_002',
membreNom: 'Martin',
membrePrenom: 'Marie',
montant: 50000,
dateEcheance: DateTime(now.year, 12, 31),
annee: now.year,
statut: StatutCotisation.nonPayee,
),
CotisationModel(
id: 'cot_003',
membreId: 'mbr_003',
membreNom: 'Bernard',
membrePrenom: 'Pierre',
montant: 50000,
dateEcheance: DateTime(now.year - 1, 12, 31),
annee: now.year - 1,
statut: StatutCotisation.enRetard,
),
CotisationModel(
id: 'cot_004',
membreId: 'mbr_004',
membreNom: 'Dubois',
membrePrenom: 'Sophie',
montant: 50000,
dateEcheance: DateTime(now.year, 12, 31),
annee: now.year,
statut: StatutCotisation.partielle,
montantPaye: 25000,
datePaiement: DateTime(now.year, 2, 10),
methodePaiement: MethodePaiement.especes,
),
CotisationModel(
id: 'cot_005',
membreId: 'mbr_005',
membreNom: 'Petit',
membrePrenom: 'Luc',
montant: 50000,
dateEcheance: DateTime(now.year, 12, 31),
annee: now.year,
statut: StatutCotisation.payee,
montantPaye: 50000,
datePaiement: DateTime(now.year, 3, 5),
methodePaiement: MethodePaiement.mobileMoney,
),
];
}
}

View File

@@ -0,0 +1,223 @@
/// Événements pour le BLoC des cotisations
library cotisations_event;
import 'package:equatable/equatable.dart';
import '../data/models/cotisation_model.dart';
/// Classe de base pour tous les événements de cotisations
abstract class CotisationsEvent extends Equatable {
const CotisationsEvent();
@override
List<Object?> get props => [];
}
/// Charger la liste des cotisations
class LoadCotisations extends CotisationsEvent {
final int page;
final int size;
const LoadCotisations({
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [page, size];
}
/// Charger une cotisation par ID
class LoadCotisationById extends CotisationsEvent {
final String id;
const LoadCotisationById({required this.id});
@override
List<Object?> get props => [id];
}
/// Créer une nouvelle cotisation
class CreateCotisation extends CotisationsEvent {
final CotisationModel cotisation;
const CreateCotisation({required this.cotisation});
@override
List<Object?> get props => [cotisation];
}
/// Mettre à jour une cotisation
class UpdateCotisation extends CotisationsEvent {
final String id;
final CotisationModel cotisation;
const UpdateCotisation({
required this.id,
required this.cotisation,
});
@override
List<Object?> get props => [id, cotisation];
}
/// Supprimer une cotisation
class DeleteCotisation extends CotisationsEvent {
final String id;
const DeleteCotisation({required this.id});
@override
List<Object?> get props => [id];
}
/// Rechercher des cotisations
class SearchCotisations extends CotisationsEvent {
final String? membreId;
final StatutCotisation? statut;
final TypeCotisation? type;
final int? annee;
final int page;
final int size;
const SearchCotisations({
this.membreId,
this.statut,
this.type,
this.annee,
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [membreId, statut, type, annee, page, size];
}
/// Charger les cotisations d'un membre
class LoadCotisationsByMembre extends CotisationsEvent {
final String membreId;
final int page;
final int size;
const LoadCotisationsByMembre({
required this.membreId,
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [membreId, page, size];
}
/// Charger les cotisations payées
class LoadCotisationsPayees extends CotisationsEvent {
final int page;
final int size;
const LoadCotisationsPayees({
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [page, size];
}
/// Charger les cotisations non payées
class LoadCotisationsNonPayees extends CotisationsEvent {
final int page;
final int size;
const LoadCotisationsNonPayees({
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [page, size];
}
/// Charger les cotisations en retard
class LoadCotisationsEnRetard extends CotisationsEvent {
final int page;
final int size;
const LoadCotisationsEnRetard({
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [page, size];
}
/// Enregistrer un paiement
class EnregistrerPaiement extends CotisationsEvent {
final String cotisationId;
final double montant;
final MethodePaiement methodePaiement;
final String? numeroPaiement;
final String? referencePaiement;
final DateTime datePaiement;
final String? notes;
final String? reference;
const EnregistrerPaiement({
required this.cotisationId,
required this.montant,
required this.methodePaiement,
this.numeroPaiement,
this.referencePaiement,
required this.datePaiement,
this.notes,
this.reference,
});
@override
List<Object?> get props => [
cotisationId,
montant,
methodePaiement,
numeroPaiement,
referencePaiement,
datePaiement,
notes,
reference,
];
}
/// Charger les statistiques des cotisations
class LoadCotisationsStats extends CotisationsEvent {
final int? annee;
const LoadCotisationsStats({this.annee});
@override
List<Object?> get props => [annee];
}
/// Générer les cotisations annuelles
class GenererCotisationsAnnuelles extends CotisationsEvent {
final int annee;
final double montant;
final DateTime dateEcheance;
const GenererCotisationsAnnuelles({
required this.annee,
required this.montant,
required this.dateEcheance,
});
@override
List<Object?> get props => [annee, montant, dateEcheance];
}
/// Envoyer un rappel de paiement
class EnvoyerRappelPaiement extends CotisationsEvent {
final String cotisationId;
const EnvoyerRappelPaiement({required this.cotisationId});
@override
List<Object?> get props => [cotisationId];
}

View File

@@ -0,0 +1,172 @@
/// États pour le BLoC des cotisations
library cotisations_state;
import 'package:equatable/equatable.dart';
import '../data/models/cotisation_model.dart';
/// Classe de base pour tous les états de cotisations
abstract class CotisationsState extends Equatable {
const CotisationsState();
@override
List<Object?> get props => [];
}
/// État initial
class CotisationsInitial extends CotisationsState {
const CotisationsInitial();
}
/// État de chargement
class CotisationsLoading extends CotisationsState {
final String? message;
const CotisationsLoading({this.message});
@override
List<Object?> get props => [message];
}
/// État de rafraîchissement
class CotisationsRefreshing extends CotisationsState {
const CotisationsRefreshing();
}
/// État chargé avec succès
class CotisationsLoaded extends CotisationsState {
final List<CotisationModel> cotisations;
final int total;
final int page;
final int size;
final int totalPages;
const CotisationsLoaded({
required this.cotisations,
required this.total,
required this.page,
required this.size,
required this.totalPages,
});
@override
List<Object?> get props => [cotisations, total, page, size, totalPages];
}
/// État détail d'une cotisation chargé
class CotisationDetailLoaded extends CotisationsState {
final CotisationModel cotisation;
const CotisationDetailLoaded({required this.cotisation});
@override
List<Object?> get props => [cotisation];
}
/// État cotisation créée
class CotisationCreated extends CotisationsState {
final CotisationModel cotisation;
const CotisationCreated({required this.cotisation});
@override
List<Object?> get props => [cotisation];
}
/// État cotisation mise à jour
class CotisationUpdated extends CotisationsState {
final CotisationModel cotisation;
const CotisationUpdated({required this.cotisation});
@override
List<Object?> get props => [cotisation];
}
/// État cotisation supprimée
class CotisationDeleted extends CotisationsState {
final String id;
const CotisationDeleted({required this.id});
@override
List<Object?> get props => [id];
}
/// État paiement enregistré
class PaiementEnregistre extends CotisationsState {
final CotisationModel cotisation;
const PaiementEnregistre({required this.cotisation});
@override
List<Object?> get props => [cotisation];
}
/// État statistiques chargées
class CotisationsStatsLoaded extends CotisationsState {
final Map<String, dynamic> stats;
const CotisationsStatsLoaded({required this.stats});
@override
List<Object?> get props => [stats];
}
/// État cotisations générées
class CotisationsGenerees extends CotisationsState {
final int nombreGenere;
const CotisationsGenerees({required this.nombreGenere});
@override
List<Object?> get props => [nombreGenere];
}
/// État rappel envoyé
class RappelEnvoye extends CotisationsState {
final String cotisationId;
const RappelEnvoye({required this.cotisationId});
@override
List<Object?> get props => [cotisationId];
}
/// État d'erreur générique
class CotisationsError extends CotisationsState {
final String message;
final dynamic error;
const CotisationsError({
required this.message,
this.error,
});
@override
List<Object?> get props => [message, error];
}
/// État d'erreur réseau
class CotisationsNetworkError extends CotisationsState {
final String message;
const CotisationsNetworkError({required this.message});
@override
List<Object?> get props => [message];
}
/// État d'erreur de validation
class CotisationsValidationError extends CotisationsState {
final String message;
final Map<String, String>? fieldErrors;
const CotisationsValidationError({
required this.message,
this.fieldErrors,
});
@override
List<Object?> get props => [message, fieldErrors];
}

View File

@@ -0,0 +1,316 @@
/// Modèle de données pour les cotisations
library cotisation_model;
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'cotisation_model.g.dart';
/// Statut d'une cotisation
enum StatutCotisation {
@JsonValue('PAYEE')
payee,
@JsonValue('NON_PAYEE')
nonPayee,
@JsonValue('EN_RETARD')
enRetard,
@JsonValue('PARTIELLE')
partielle,
@JsonValue('ANNULEE')
annulee,
}
/// Type de cotisation
enum TypeCotisation {
@JsonValue('ANNUELLE')
annuelle,
@JsonValue('MENSUELLE')
mensuelle,
@JsonValue('TRIMESTRIELLE')
trimestrielle,
@JsonValue('SEMESTRIELLE')
semestrielle,
@JsonValue('EXCEPTIONNELLE')
exceptionnelle,
}
/// Méthode de paiement
enum MethodePaiement {
@JsonValue('ESPECES')
especes,
@JsonValue('CHEQUE')
cheque,
@JsonValue('VIREMENT')
virement,
@JsonValue('CARTE_BANCAIRE')
carteBancaire,
@JsonValue('WAVE_MONEY')
waveMoney,
@JsonValue('ORANGE_MONEY')
orangeMoney,
@JsonValue('FREE_MONEY')
freeMoney,
@JsonValue('MOBILE_MONEY')
mobileMoney,
@JsonValue('AUTRE')
autre,
}
/// Modèle complet d'une cotisation
@JsonSerializable(explicitToJson: true)
class CotisationModel extends Equatable {
/// Identifiant unique
final String? id;
/// Membre concerné
final String membreId;
final String? membreNom;
final String? membrePrenom;
/// Organisation
final String? organisationId;
final String? organisationNom;
/// Informations de la cotisation
final TypeCotisation type;
final StatutCotisation statut;
final double montant;
final double? montantPaye;
final String devise;
/// Dates
final DateTime dateEcheance;
final DateTime? datePaiement;
final DateTime? dateRappel;
/// Paiement
final MethodePaiement? methodePaiement;
final String? numeroPaiement;
final String? referencePaiement;
/// Période
final int annee;
final int? mois;
final int? trimestre;
final int? semestre;
/// Informations complémentaires
final String? description;
final String? notes;
final String? recu;
/// Métadonnées
final DateTime? dateCreation;
final DateTime? dateModification;
final String? creeParId;
final String? modifieParId;
const CotisationModel({
this.id,
required this.membreId,
this.membreNom,
this.membrePrenom,
this.organisationId,
this.organisationNom,
this.type = TypeCotisation.annuelle,
this.statut = StatutCotisation.nonPayee,
required this.montant,
this.montantPaye,
this.devise = 'XOF',
required this.dateEcheance,
this.datePaiement,
this.dateRappel,
this.methodePaiement,
this.numeroPaiement,
this.referencePaiement,
required this.annee,
this.mois,
this.trimestre,
this.semestre,
this.description,
this.notes,
this.recu,
this.dateCreation,
this.dateModification,
this.creeParId,
this.modifieParId,
});
/// Désérialisation depuis JSON
factory CotisationModel.fromJson(Map<String, dynamic> json) =>
_$CotisationModelFromJson(json);
/// Sérialisation vers JSON
Map<String, dynamic> toJson() => _$CotisationModelToJson(this);
/// Copie avec modifications
CotisationModel copyWith({
String? id,
String? membreId,
String? membreNom,
String? membrePrenom,
String? organisationId,
String? organisationNom,
TypeCotisation? type,
StatutCotisation? statut,
double? montant,
double? montantPaye,
String? devise,
DateTime? dateEcheance,
DateTime? datePaiement,
DateTime? dateRappel,
MethodePaiement? methodePaiement,
String? numeroPaiement,
String? referencePaiement,
int? annee,
int? mois,
int? trimestre,
int? semestre,
String? description,
String? notes,
String? recu,
DateTime? dateCreation,
DateTime? dateModification,
String? creeParId,
String? modifieParId,
}) {
return CotisationModel(
id: id ?? this.id,
membreId: membreId ?? this.membreId,
membreNom: membreNom ?? this.membreNom,
membrePrenom: membrePrenom ?? this.membrePrenom,
organisationId: organisationId ?? this.organisationId,
organisationNom: organisationNom ?? this.organisationNom,
type: type ?? this.type,
statut: statut ?? this.statut,
montant: montant ?? this.montant,
montantPaye: montantPaye ?? this.montantPaye,
devise: devise ?? this.devise,
dateEcheance: dateEcheance ?? this.dateEcheance,
datePaiement: datePaiement ?? this.datePaiement,
dateRappel: dateRappel ?? this.dateRappel,
methodePaiement: methodePaiement ?? this.methodePaiement,
numeroPaiement: numeroPaiement ?? this.numeroPaiement,
referencePaiement: referencePaiement ?? this.referencePaiement,
annee: annee ?? this.annee,
mois: mois ?? this.mois,
trimestre: trimestre ?? this.trimestre,
semestre: semestre ?? this.semestre,
description: description ?? this.description,
notes: notes ?? this.notes,
recu: recu ?? this.recu,
dateCreation: dateCreation ?? this.dateCreation,
dateModification: dateModification ?? this.dateModification,
creeParId: creeParId ?? this.creeParId,
modifieParId: modifieParId ?? this.modifieParId,
);
}
/// Nom complet du membre
String get membreNomComplet {
if (membreNom != null && membrePrenom != null) {
return '$membrePrenom $membreNom';
}
return membreNom ?? membrePrenom ?? 'Membre inconnu';
}
/// Montant restant à payer
double get montantRestant {
if (montantPaye == null) return montant;
return montant - montantPaye!;
}
/// Pourcentage payé
double get pourcentagePaye {
if (montantPaye == null || montant == 0) return 0;
return (montantPaye! / montant) * 100;
}
/// Vérifie si la cotisation est payée
bool get estPayee => statut == StatutCotisation.payee;
/// Vérifie si la cotisation est en retard
bool get estEnRetard {
if (estPayee) return false;
return DateTime.now().isAfter(dateEcheance);
}
/// Nombre de jours avant/après l'échéance
int get joursAvantEcheance {
return dateEcheance.difference(DateTime.now()).inDays;
}
/// Libellé de la période
String get libellePeriode {
switch (type) {
case TypeCotisation.annuelle:
return 'Année $annee';
case TypeCotisation.mensuelle:
if (mois != null) {
return '${_getNomMois(mois!)} $annee';
}
return 'Année $annee';
case TypeCotisation.trimestrielle:
if (trimestre != null) {
return 'T$trimestre $annee';
}
return 'Année $annee';
case TypeCotisation.semestrielle:
if (semestre != null) {
return 'S$semestre $annee';
}
return 'Année $annee';
case TypeCotisation.exceptionnelle:
return 'Exceptionnelle $annee';
}
}
/// Nom du mois
String _getNomMois(int mois) {
const mois_fr = [
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
];
if (mois >= 1 && mois <= 12) {
return mois_fr[mois - 1];
}
return 'Mois $mois';
}
@override
List<Object?> get props => [
id,
membreId,
membreNom,
membrePrenom,
organisationId,
organisationNom,
type,
statut,
montant,
montantPaye,
devise,
dateEcheance,
datePaiement,
dateRappel,
methodePaiement,
numeroPaiement,
referencePaiement,
annee,
mois,
trimestre,
semestre,
description,
notes,
recu,
dateCreation,
dateModification,
creeParId,
modifieParId,
];
@override
String toString() =>
'CotisationModel(id: $id, membre: $membreNomComplet, montant: $montant $devise, statut: $statut)';
}

View File

@@ -0,0 +1,110 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cotisation_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CotisationModel _$CotisationModelFromJson(Map<String, dynamic> json) =>
CotisationModel(
id: json['id'] as String?,
membreId: json['membreId'] as String,
membreNom: json['membreNom'] as String?,
membrePrenom: json['membrePrenom'] as String?,
organisationId: json['organisationId'] as String?,
organisationNom: json['organisationNom'] as String?,
type: $enumDecodeNullable(_$TypeCotisationEnumMap, json['type']) ??
TypeCotisation.annuelle,
statut: $enumDecodeNullable(_$StatutCotisationEnumMap, json['statut']) ??
StatutCotisation.nonPayee,
montant: (json['montant'] as num).toDouble(),
montantPaye: (json['montantPaye'] as num?)?.toDouble(),
devise: json['devise'] as String? ?? 'XOF',
dateEcheance: DateTime.parse(json['dateEcheance'] as String),
datePaiement: json['datePaiement'] == null
? null
: DateTime.parse(json['datePaiement'] as String),
dateRappel: json['dateRappel'] == null
? null
: DateTime.parse(json['dateRappel'] as String),
methodePaiement: $enumDecodeNullable(
_$MethodePaiementEnumMap, json['methodePaiement']),
numeroPaiement: json['numeroPaiement'] as String?,
referencePaiement: json['referencePaiement'] as String?,
annee: (json['annee'] as num).toInt(),
mois: (json['mois'] as num?)?.toInt(),
trimestre: (json['trimestre'] as num?)?.toInt(),
semestre: (json['semestre'] as num?)?.toInt(),
description: json['description'] as String?,
notes: json['notes'] as String?,
recu: json['recu'] as String?,
dateCreation: json['dateCreation'] == null
? null
: DateTime.parse(json['dateCreation'] as String),
dateModification: json['dateModification'] == null
? null
: DateTime.parse(json['dateModification'] as String),
creeParId: json['creeParId'] as String?,
modifieParId: json['modifieParId'] as String?,
);
Map<String, dynamic> _$CotisationModelToJson(CotisationModel instance) =>
<String, dynamic>{
'id': instance.id,
'membreId': instance.membreId,
'membreNom': instance.membreNom,
'membrePrenom': instance.membrePrenom,
'organisationId': instance.organisationId,
'organisationNom': instance.organisationNom,
'type': _$TypeCotisationEnumMap[instance.type]!,
'statut': _$StatutCotisationEnumMap[instance.statut]!,
'montant': instance.montant,
'montantPaye': instance.montantPaye,
'devise': instance.devise,
'dateEcheance': instance.dateEcheance.toIso8601String(),
'datePaiement': instance.datePaiement?.toIso8601String(),
'dateRappel': instance.dateRappel?.toIso8601String(),
'methodePaiement': _$MethodePaiementEnumMap[instance.methodePaiement],
'numeroPaiement': instance.numeroPaiement,
'referencePaiement': instance.referencePaiement,
'annee': instance.annee,
'mois': instance.mois,
'trimestre': instance.trimestre,
'semestre': instance.semestre,
'description': instance.description,
'notes': instance.notes,
'recu': instance.recu,
'dateCreation': instance.dateCreation?.toIso8601String(),
'dateModification': instance.dateModification?.toIso8601String(),
'creeParId': instance.creeParId,
'modifieParId': instance.modifieParId,
};
const _$TypeCotisationEnumMap = {
TypeCotisation.annuelle: 'ANNUELLE',
TypeCotisation.mensuelle: 'MENSUELLE',
TypeCotisation.trimestrielle: 'TRIMESTRIELLE',
TypeCotisation.semestrielle: 'SEMESTRIELLE',
TypeCotisation.exceptionnelle: 'EXCEPTIONNELLE',
};
const _$StatutCotisationEnumMap = {
StatutCotisation.payee: 'PAYEE',
StatutCotisation.nonPayee: 'NON_PAYEE',
StatutCotisation.enRetard: 'EN_RETARD',
StatutCotisation.partielle: 'PARTIELLE',
StatutCotisation.annulee: 'ANNULEE',
};
const _$MethodePaiementEnumMap = {
MethodePaiement.especes: 'ESPECES',
MethodePaiement.cheque: 'CHEQUE',
MethodePaiement.virement: 'VIREMENT',
MethodePaiement.carteBancaire: 'CARTE_BANCAIRE',
MethodePaiement.waveMoney: 'WAVE_MONEY',
MethodePaiement.orangeMoney: 'ORANGE_MONEY',
MethodePaiement.freeMoney: 'FREE_MONEY',
MethodePaiement.mobileMoney: 'MOBILE_MONEY',
MethodePaiement.autre: 'AUTRE',
};

View File

@@ -0,0 +1,19 @@
/// Configuration de l'injection de dépendances pour le module Cotisations
library cotisations_di;
import 'package:get_it/get_it.dart';
import '../bloc/cotisations_bloc.dart';
/// Enregistrer les dépendances du module Cotisations
void registerCotisationsDependencies(GetIt getIt) {
// BLoC
getIt.registerFactory<CotisationsBloc>(
() => CotisationsBloc(),
);
// Repository sera ajouté ici quand l'API backend sera prête
// getIt.registerLazySingleton<CotisationRepository>(
// () => CotisationRepositoryImpl(dio: getIt()),
// );
}

View File

@@ -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),
),
],
),
);
}
}

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/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(),
);
}
}

View File

@@ -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,
),
);
}
}
}

View File

@@ -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,
),
);
}
}
}