Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
/// BLoC pour la gestion des contributions
|
||||
library contributions_bloc;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../data/models/contribution_model.dart';
|
||||
import 'contributions_event.dart';
|
||||
import 'contributions_state.dart';
|
||||
|
||||
/// BLoC pour gérer l'état des contributions
|
||||
class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
|
||||
ContributionsBloc() : super(const ContributionsInitial()) {
|
||||
on<LoadContributions>(_onLoadContributions);
|
||||
on<LoadContributionById>(_onLoadContributionById);
|
||||
on<CreateContribution>(_onCreateContribution);
|
||||
on<UpdateContribution>(_onUpdateContribution);
|
||||
on<DeleteContribution>(_onDeleteContribution);
|
||||
on<SearchContributions>(_onSearchContributions);
|
||||
on<LoadContributionsByMembre>(_onLoadContributionsByMembre);
|
||||
on<LoadContributionsPayees>(_onLoadContributionsPayees);
|
||||
on<LoadContributionsNonPayees>(_onLoadContributionsNonPayees);
|
||||
on<LoadContributionsEnRetard>(_onLoadContributionsEnRetard);
|
||||
on<RecordPayment>(_onRecordPayment);
|
||||
on<LoadContributionsStats>(_onLoadContributionsStats);
|
||||
on<GenerateAnnualContributions>(_onGenerateAnnualContributions);
|
||||
on<SendPaymentReminder>(_onSendPaymentReminder);
|
||||
}
|
||||
|
||||
/// Charger la liste des contributions
|
||||
Future<void> _onLoadContributions(
|
||||
LoadContributions event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'LoadContributions', data: {
|
||||
'page': event.page,
|
||||
'size': event.size,
|
||||
});
|
||||
|
||||
emit(const ContributionsLoading(message: 'Chargement des contributions...'));
|
||||
|
||||
// Simuler un délai réseau
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Données mock
|
||||
final contributions = _getMockContributions();
|
||||
final total = contributions.length;
|
||||
final totalPages = (total / event.size).ceil();
|
||||
|
||||
// Pagination
|
||||
final start = event.page * event.size;
|
||||
final end = (start + event.size).clamp(0, total);
|
||||
final paginatedContributions = contributions.sublist(
|
||||
start.clamp(0, total),
|
||||
end,
|
||||
);
|
||||
|
||||
emit(ContributionsLoaded(
|
||||
contributions: paginatedContributions,
|
||||
total: total,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
totalPages: totalPages,
|
||||
));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionsLoaded', data: {
|
||||
'count': paginatedContributions.length,
|
||||
'total': total,
|
||||
});
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors du chargement des contributions',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Erreur lors du chargement des contributions',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger une contribution par ID
|
||||
Future<void> _onLoadContributionById(
|
||||
LoadContributionById event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'LoadContributionById', data: {
|
||||
'id': event.id,
|
||||
});
|
||||
|
||||
emit(const ContributionsLoading(message: 'Chargement de la contribution...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
final contributions = _getMockContributions();
|
||||
final contribution = contributions.firstWhere(
|
||||
(c) => c.id == event.id,
|
||||
orElse: () => throw Exception('Contribution non trouvée'),
|
||||
);
|
||||
|
||||
emit(ContributionDetailLoaded(contribution: contribution));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionDetailLoaded');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors du chargement de la contribution',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Contribution non trouvée',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Créer une nouvelle contribution
|
||||
Future<void> _onCreateContribution(
|
||||
CreateContribution event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'CreateContribution');
|
||||
|
||||
emit(const ContributionsLoading(message: 'Création de la contribution...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final newContribution = event.contribution.copyWith(
|
||||
id: 'cont_${DateTime.now().millisecondsSinceEpoch}',
|
||||
dateCreation: DateTime.now(),
|
||||
);
|
||||
|
||||
emit(ContributionCreated(contribution: newContribution));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionCreated');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors de la création de la contribution',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Erreur lors de la création de la contribution',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Mettre à jour une contribution
|
||||
Future<void> _onUpdateContribution(
|
||||
UpdateContribution event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'UpdateContribution', data: {
|
||||
'id': event.id,
|
||||
});
|
||||
|
||||
emit(const ContributionsLoading(message: 'Mise à jour de la contribution...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final updatedContribution = event.contribution.copyWith(
|
||||
id: event.id,
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
|
||||
emit(ContributionUpdated(contribution: updatedContribution));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionUpdated');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors de la mise à jour de la contribution',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Erreur lors de la mise à jour de la contribution',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprimer une contribution
|
||||
Future<void> _onDeleteContribution(
|
||||
DeleteContribution event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'DeleteContribution', data: {
|
||||
'id': event.id,
|
||||
});
|
||||
|
||||
emit(const ContributionsLoading(message: 'Suppression de la contribution...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
emit(ContributionDeleted(id: event.id));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionDeleted');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors de la suppression de la contribution',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Erreur lors de la suppression de la contribution',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Rechercher des contributions
|
||||
Future<void> _onSearchContributions(
|
||||
SearchContributions event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'SearchContributions');
|
||||
|
||||
emit(const ContributionsLoading(message: 'Recherche en cours...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
var contributions = _getMockContributions();
|
||||
|
||||
// Filtrer par membre
|
||||
if (event.membreId != null) {
|
||||
contributions = contributions
|
||||
.where((c) => c.membreId == event.membreId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtrer par statut
|
||||
if (event.statut != null) {
|
||||
contributions = contributions
|
||||
.where((c) => c.statut == event.statut)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtrer par type
|
||||
if (event.type != null) {
|
||||
contributions = contributions
|
||||
.where((c) => c.type == event.type)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtrer par année
|
||||
if (event.annee != null) {
|
||||
contributions = contributions
|
||||
.where((c) => c.annee == event.annee)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final total = contributions.length;
|
||||
final totalPages = (total / event.size).ceil();
|
||||
|
||||
// Pagination
|
||||
final start = event.page * event.size;
|
||||
final end = (start + event.size).clamp(0, total);
|
||||
final paginatedContributions = contributions.sublist(
|
||||
start.clamp(0, total),
|
||||
end,
|
||||
);
|
||||
|
||||
emit(ContributionsLoaded(
|
||||
contributions: paginatedContributions,
|
||||
total: total,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
totalPages: totalPages,
|
||||
));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionsLoaded (search)');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors de la recherche de contributions',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Erreur lors de la recherche',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger les contributions d'un membre
|
||||
Future<void> _onLoadContributionsByMembre(
|
||||
LoadContributionsByMembre event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'LoadContributionsByMembre', data: {
|
||||
'membreId': event.membreId,
|
||||
});
|
||||
|
||||
emit(const ContributionsLoading(message: 'Chargement des contributions du membre...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final contributions = _getMockContributions()
|
||||
.where((c) => c.membreId == event.membreId)
|
||||
.toList();
|
||||
|
||||
final total = contributions.length;
|
||||
final totalPages = (total / event.size).ceil();
|
||||
|
||||
emit(ContributionsLoaded(
|
||||
contributions: contributions,
|
||||
total: total,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
totalPages: totalPages,
|
||||
));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'ContributionsLoaded (by membre)');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error(
|
||||
'Erreur lors du chargement des contributions du membre',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(ContributionsError(
|
||||
message: 'Erreur lors du chargement',
|
||||
error: e,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger les contributions payées
|
||||
Future<void> _onLoadContributionsPayees(
|
||||
LoadContributionsPayees event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ContributionsLoading(message: 'Chargement des contributions payées...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final contributions = _getMockContributions()
|
||||
.where((c) => c.statut == ContributionStatus.payee)
|
||||
.toList();
|
||||
|
||||
final total = contributions.length;
|
||||
final totalPages = (total / event.size).ceil();
|
||||
|
||||
emit(ContributionsLoaded(
|
||||
contributions: contributions,
|
||||
total: total,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
totalPages: totalPages,
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger les contributions non payées
|
||||
Future<void> _onLoadContributionsNonPayees(
|
||||
LoadContributionsNonPayees event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ContributionsLoading(message: 'Chargement des contributions non payées...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final contributions = _getMockContributions()
|
||||
.where((c) => c.statut == ContributionStatus.nonPayee)
|
||||
.toList();
|
||||
|
||||
final total = contributions.length;
|
||||
final totalPages = (total / event.size).ceil();
|
||||
|
||||
emit(ContributionsLoaded(
|
||||
contributions: contributions,
|
||||
total: total,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
totalPages: totalPages,
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger les contributions en retard
|
||||
Future<void> _onLoadContributionsEnRetard(
|
||||
LoadContributionsEnRetard event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ContributionsLoading(message: 'Chargement des contributions en retard...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final contributions = _getMockContributions()
|
||||
.where((c) => c.statut == ContributionStatus.enRetard)
|
||||
.toList();
|
||||
|
||||
final total = contributions.length;
|
||||
final totalPages = (total / event.size).ceil();
|
||||
|
||||
emit(ContributionsLoaded(
|
||||
contributions: contributions,
|
||||
total: total,
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
totalPages: totalPages,
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistrer un paiement
|
||||
Future<void> _onRecordPayment(
|
||||
RecordPayment event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
AppLogger.blocEvent('ContributionsBloc', 'RecordPayment');
|
||||
|
||||
emit(const ContributionsLoading(message: 'Enregistrement du paiement...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final contributions = _getMockContributions();
|
||||
final contribution = contributions.firstWhere((c) => c.id == event.contributionId);
|
||||
|
||||
final updatedContribution = contribution.copyWith(
|
||||
montantPaye: event.montant,
|
||||
datePaiement: event.datePaiement,
|
||||
methodePaiement: event.methodePaiement,
|
||||
numeroPaiement: event.numeroPaiement,
|
||||
referencePaiement: event.referencePaiement,
|
||||
statut: event.montant >= contribution.montant
|
||||
? ContributionStatus.payee
|
||||
: ContributionStatus.partielle,
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
|
||||
emit(PaymentRecorded(contribution: updatedContribution));
|
||||
|
||||
AppLogger.blocState('ContributionsBloc', 'PaymentRecorded');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur lors de l\'enregistrement du paiement', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger les statistiques
|
||||
Future<void> _onLoadContributionsStats(
|
||||
LoadContributionsStats event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ContributionsLoading(message: 'Chargement des statistiques...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
final contributions = _getMockContributions();
|
||||
|
||||
final stats = {
|
||||
'total': contributions.length,
|
||||
'payees': contributions.where((c) => c.statut == ContributionStatus.payee).length,
|
||||
'nonPayees': contributions.where((c) => c.statut == ContributionStatus.nonPayee).length,
|
||||
'enRetard': contributions.where((c) => c.statut == ContributionStatus.enRetard).length,
|
||||
'partielles': contributions.where((c) => c.statut == ContributionStatus.partielle).length,
|
||||
'montantTotal': contributions.fold<double>(0, (sum, c) => sum + c.montant),
|
||||
'montantPaye': contributions.fold<double>(0, (sum, c) => sum + (c.montantPaye ?? 0)),
|
||||
'montantRestant': contributions.fold<double>(0, (sum, c) => sum + c.montantRestant),
|
||||
'tauxRecouvrement': 0.0,
|
||||
};
|
||||
|
||||
if (stats['montantTotal']! > 0) {
|
||||
stats['tauxRecouvrement'] = (stats['montantPaye']! / stats['montantTotal']!) * 100;
|
||||
}
|
||||
|
||||
emit(ContributionsStatsLoaded(stats: stats));
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Générer les contributions annuelles
|
||||
Future<void> _onGenerateAnnualContributions(
|
||||
GenerateAnnualContributions event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ContributionsLoading(message: 'Génération des contributions...'));
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// Simuler la génération de 50 contributions
|
||||
emit(const ContributionsGenerated(nombreGenere: 50));
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoyer un rappel de paiement
|
||||
Future<void> _onSendPaymentReminder(
|
||||
SendPaymentReminder event,
|
||||
Emitter<ContributionsState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ContributionsLoading(message: 'Envoi du rappel...'));
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
emit(ReminderSent(contributionId: event.contributionId));
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
|
||||
emit(ContributionsError(message: 'Erreur', error: e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Données mock pour les tests
|
||||
List<ContributionModel> _getMockContributions() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
ContributionModel(
|
||||
id: 'cont_001',
|
||||
membreId: 'mbr_001',
|
||||
membreNom: 'Dupont',
|
||||
membrePrenom: 'Jean',
|
||||
montant: 50000,
|
||||
dateEcheance: DateTime(now.year, 12, 31),
|
||||
annee: now.year,
|
||||
statut: ContributionStatus.payee,
|
||||
montantPaye: 50000,
|
||||
datePaiement: DateTime(now.year, 1, 15),
|
||||
methodePaiement: PaymentMethod.virement,
|
||||
),
|
||||
ContributionModel(
|
||||
id: 'cont_002',
|
||||
membreId: 'mbr_002',
|
||||
membreNom: 'Martin',
|
||||
membrePrenom: 'Marie',
|
||||
montant: 50000,
|
||||
dateEcheance: DateTime(now.year, 12, 31),
|
||||
annee: now.year,
|
||||
statut: ContributionStatus.nonPayee,
|
||||
),
|
||||
ContributionModel(
|
||||
id: 'cont_003',
|
||||
membreId: 'mbr_003',
|
||||
membreNom: 'Bernard',
|
||||
membrePrenom: 'Pierre',
|
||||
montant: 50000,
|
||||
dateEcheance: DateTime(now.year - 1, 12, 31),
|
||||
annee: now.year - 1,
|
||||
statut: ContributionStatus.enRetard,
|
||||
),
|
||||
ContributionModel(
|
||||
id: 'cont_004',
|
||||
membreId: 'mbr_004',
|
||||
membreNom: 'Dubois',
|
||||
membrePrenom: 'Sophie',
|
||||
montant: 50000,
|
||||
dateEcheance: DateTime(now.year, 12, 31),
|
||||
annee: now.year,
|
||||
statut: ContributionStatus.partielle,
|
||||
montantPaye: 25000,
|
||||
datePaiement: DateTime(now.year, 2, 10),
|
||||
methodePaiement: PaymentMethod.especes,
|
||||
),
|
||||
ContributionModel(
|
||||
id: 'cont_005',
|
||||
membreId: 'mbr_005',
|
||||
membreNom: 'Petit',
|
||||
membrePrenom: 'Luc',
|
||||
montant: 50000,
|
||||
dateEcheance: DateTime(now.year, 12, 31),
|
||||
annee: now.year,
|
||||
statut: ContributionStatus.payee,
|
||||
montantPaye: 50000,
|
||||
datePaiement: DateTime(now.year, 3, 5),
|
||||
methodePaiement: PaymentMethod.mobileMoney,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/// Événements pour le BLoC des contributions
|
||||
library contributions_event;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../data/models/contribution_model.dart';
|
||||
|
||||
/// Classe de base pour tous les événements de contributions
|
||||
abstract class ContributionsEvent extends Equatable {
|
||||
const ContributionsEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Charger la liste des contributions
|
||||
class LoadContributions extends ContributionsEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
const LoadContributions({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size];
|
||||
}
|
||||
|
||||
/// Charger une contribution par ID
|
||||
class LoadContributionById extends ContributionsEvent {
|
||||
final String id;
|
||||
|
||||
const LoadContributionById({required this.id});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Créer une nouvelle contribution
|
||||
class CreateContribution extends ContributionsEvent {
|
||||
final ContributionModel contribution;
|
||||
|
||||
const CreateContribution({required this.contribution});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contribution];
|
||||
}
|
||||
|
||||
/// Mettre à jour une contribution
|
||||
class UpdateContribution extends ContributionsEvent {
|
||||
final String id;
|
||||
final ContributionModel contribution;
|
||||
|
||||
const UpdateContribution({
|
||||
required this.id,
|
||||
required this.contribution,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, contribution];
|
||||
}
|
||||
|
||||
/// Supprimer une contribution
|
||||
class DeleteContribution extends ContributionsEvent {
|
||||
final String id;
|
||||
|
||||
const DeleteContribution({required this.id});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// Rechercher des contributions
|
||||
class SearchContributions extends ContributionsEvent {
|
||||
final String? membreId;
|
||||
final ContributionStatus? statut;
|
||||
final ContributionType? type;
|
||||
final int? annee;
|
||||
final String? query;
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
const SearchContributions({
|
||||
this.membreId,
|
||||
this.statut,
|
||||
this.type,
|
||||
this.annee,
|
||||
this.query,
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, statut, type, annee, query, page, size];
|
||||
}
|
||||
|
||||
/// Charger les contributions d'un membre
|
||||
class LoadContributionsByMembre extends ContributionsEvent {
|
||||
final String membreId;
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
const LoadContributionsByMembre({
|
||||
required this.membreId,
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, page, size];
|
||||
}
|
||||
|
||||
/// Charger les contributions payées
|
||||
class LoadContributionsPayees extends ContributionsEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
const LoadContributionsPayees({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size];
|
||||
}
|
||||
|
||||
/// Charger les contributions non payées
|
||||
class LoadContributionsNonPayees extends ContributionsEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
const LoadContributionsNonPayees({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size];
|
||||
}
|
||||
|
||||
/// Charger les contributions en retard
|
||||
class LoadContributionsEnRetard extends ContributionsEvent {
|
||||
final int page;
|
||||
final int size;
|
||||
|
||||
const LoadContributionsEnRetard({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size];
|
||||
}
|
||||
|
||||
/// Enregistrer un paiement
|
||||
class RecordPayment extends ContributionsEvent {
|
||||
final String contributionId;
|
||||
final double montant;
|
||||
final PaymentMethod methodePaiement;
|
||||
final String? numeroPaiement;
|
||||
final String? referencePaiement;
|
||||
final DateTime datePaiement;
|
||||
final String? notes;
|
||||
final String? reference;
|
||||
|
||||
const RecordPayment({
|
||||
required this.contributionId,
|
||||
required this.montant,
|
||||
required this.methodePaiement,
|
||||
this.numeroPaiement,
|
||||
this.referencePaiement,
|
||||
required this.datePaiement,
|
||||
this.notes,
|
||||
this.reference,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
contributionId,
|
||||
montant,
|
||||
methodePaiement,
|
||||
numeroPaiement,
|
||||
referencePaiement,
|
||||
datePaiement,
|
||||
notes,
|
||||
reference,
|
||||
];
|
||||
}
|
||||
|
||||
/// Charger les statistiques des contributions
|
||||
class LoadContributionsStats extends ContributionsEvent {
|
||||
final int? annee;
|
||||
|
||||
const LoadContributionsStats({this.annee});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [annee];
|
||||
}
|
||||
|
||||
/// Générer les contributions annuelles
|
||||
class GenerateAnnualContributions extends ContributionsEvent {
|
||||
final int annee;
|
||||
final double montant;
|
||||
final DateTime dateEcheance;
|
||||
|
||||
const GenerateAnnualContributions({
|
||||
required this.annee,
|
||||
required this.montant,
|
||||
required this.dateEcheance,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [annee, montant, dateEcheance];
|
||||
}
|
||||
|
||||
/// Envoyer un rappel de paiement
|
||||
class SendPaymentReminder extends ContributionsEvent {
|
||||
final String contributionId;
|
||||
|
||||
const SendPaymentReminder({required this.contributionId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contributionId];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/// États pour le BLoC des contributions
|
||||
library contributions_state;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../data/models/contribution_model.dart';
|
||||
|
||||
/// Classe de base pour tous les états de contributions
|
||||
abstract class ContributionsState extends Equatable {
|
||||
const ContributionsState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// État initial
|
||||
class ContributionsInitial extends ContributionsState {
|
||||
const ContributionsInitial();
|
||||
}
|
||||
|
||||
/// État de chargement
|
||||
class ContributionsLoading extends ContributionsState {
|
||||
final String? message;
|
||||
|
||||
const ContributionsLoading({this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// État de rafraîchissement
|
||||
class ContributionsRefreshing extends ContributionsState {
|
||||
const ContributionsRefreshing();
|
||||
}
|
||||
|
||||
/// État chargé avec succès
|
||||
class ContributionsLoaded extends ContributionsState {
|
||||
final List<ContributionModel> contributions;
|
||||
final int total;
|
||||
final int page;
|
||||
final int size;
|
||||
final int totalPages;
|
||||
|
||||
const ContributionsLoaded({
|
||||
required this.contributions,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contributions, total, page, size, totalPages];
|
||||
}
|
||||
|
||||
/// État détail d'une contribution chargé
|
||||
class ContributionDetailLoaded extends ContributionsState {
|
||||
final ContributionModel contribution;
|
||||
|
||||
const ContributionDetailLoaded({required this.contribution});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contribution];
|
||||
}
|
||||
|
||||
/// État contribution créée
|
||||
class ContributionCreated extends ContributionsState {
|
||||
final ContributionModel contribution;
|
||||
|
||||
const ContributionCreated({required this.contribution});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contribution];
|
||||
}
|
||||
|
||||
/// État contribution mise à jour
|
||||
class ContributionUpdated extends ContributionsState {
|
||||
final ContributionModel contribution;
|
||||
|
||||
const ContributionUpdated({required this.contribution});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contribution];
|
||||
}
|
||||
|
||||
/// État contribution supprimée
|
||||
class ContributionDeleted extends ContributionsState {
|
||||
final String id;
|
||||
|
||||
const ContributionDeleted({required this.id});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
/// État paiement enregistré
|
||||
class PaymentRecorded extends ContributionsState {
|
||||
final ContributionModel contribution;
|
||||
|
||||
const PaymentRecorded({required this.contribution});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contribution];
|
||||
}
|
||||
|
||||
/// État statistiques chargées
|
||||
class ContributionsStatsLoaded extends ContributionsState {
|
||||
final Map<String, dynamic> stats;
|
||||
|
||||
const ContributionsStatsLoaded({required this.stats});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [stats];
|
||||
}
|
||||
|
||||
/// État contributions générées
|
||||
class ContributionsGenerated extends ContributionsState {
|
||||
final int nombreGenere;
|
||||
|
||||
const ContributionsGenerated({required this.nombreGenere});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [nombreGenere];
|
||||
}
|
||||
|
||||
/// État rappel envoyé
|
||||
class ReminderSent extends ContributionsState {
|
||||
final String contributionId;
|
||||
|
||||
const ReminderSent({required this.contributionId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [contributionId];
|
||||
}
|
||||
|
||||
/// État d'erreur générique
|
||||
class ContributionsError extends ContributionsState {
|
||||
final String message;
|
||||
final dynamic error;
|
||||
|
||||
const ContributionsError({
|
||||
required this.message,
|
||||
this.error,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, error];
|
||||
}
|
||||
|
||||
/// État d'erreur réseau
|
||||
class ContributionsNetworkError extends ContributionsState {
|
||||
final String message;
|
||||
|
||||
const ContributionsNetworkError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
/// État d'erreur de validation
|
||||
class ContributionsValidationError extends ContributionsState {
|
||||
final String message;
|
||||
final Map<String, String>? fieldErrors;
|
||||
|
||||
const ContributionsValidationError({
|
||||
required this.message,
|
||||
this.fieldErrors,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, fieldErrors];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/// Modèle de données pour les contributions
|
||||
library contribution_model;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'contribution_model.g.dart';
|
||||
|
||||
/// Statut d'une contribution
|
||||
enum ContributionStatus {
|
||||
@JsonValue('PAYEE')
|
||||
payee,
|
||||
@JsonValue('NON_PAYEE')
|
||||
nonPayee,
|
||||
@JsonValue('EN_RETARD')
|
||||
enRetard,
|
||||
@JsonValue('PARTIELLE')
|
||||
partielle,
|
||||
@JsonValue('ANNULEE')
|
||||
annulee,
|
||||
}
|
||||
|
||||
/// Type de contribution
|
||||
enum ContributionType {
|
||||
@JsonValue('ANNUELLE')
|
||||
annuelle,
|
||||
@JsonValue('MENSUELLE')
|
||||
mensuelle,
|
||||
@JsonValue('TRIMESTRIELLE')
|
||||
trimestrielle,
|
||||
@JsonValue('SEMESTRIELLE')
|
||||
semestrielle,
|
||||
@JsonValue('EXCEPTIONNELLE')
|
||||
exceptionnelle,
|
||||
}
|
||||
|
||||
/// Méthode de paiement
|
||||
enum PaymentMethod {
|
||||
@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 contribution
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class ContributionModel 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 contribution
|
||||
final ContributionType type;
|
||||
final ContributionStatus statut;
|
||||
final double montant;
|
||||
final double? montantPaye;
|
||||
final String devise;
|
||||
|
||||
/// Dates
|
||||
final DateTime dateEcheance;
|
||||
final DateTime? datePaiement;
|
||||
final DateTime? dateRappel;
|
||||
|
||||
/// Paiement
|
||||
final PaymentMethod? 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 ContributionModel({
|
||||
this.id,
|
||||
required this.membreId,
|
||||
this.membreNom,
|
||||
this.membrePrenom,
|
||||
this.organisationId,
|
||||
this.organisationNom,
|
||||
this.type = ContributionType.annuelle,
|
||||
this.statut = ContributionStatus.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 ContributionModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContributionModelFromJson(json);
|
||||
|
||||
/// Sérialisation vers JSON
|
||||
Map<String, dynamic> toJson() => _$ContributionModelToJson(this);
|
||||
|
||||
/// Copie avec modifications
|
||||
ContributionModel copyWith({
|
||||
String? id,
|
||||
String? membreId,
|
||||
String? membreNom,
|
||||
String? membrePrenom,
|
||||
String? organisationId,
|
||||
String? organisationNom,
|
||||
ContributionType? type,
|
||||
ContributionStatus? statut,
|
||||
double? montant,
|
||||
double? montantPaye,
|
||||
String? devise,
|
||||
DateTime? dateEcheance,
|
||||
DateTime? datePaiement,
|
||||
DateTime? dateRappel,
|
||||
PaymentMethod? 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 ContributionModel(
|
||||
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 contribution est payée
|
||||
bool get estPayee => statut == ContributionStatus.payee;
|
||||
|
||||
/// Vérifie si la contribution 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 ContributionType.annuelle:
|
||||
return 'Année $annee';
|
||||
case ContributionType.mensuelle:
|
||||
if (mois != null) {
|
||||
return '${_getNomMois(mois!)} $annee';
|
||||
}
|
||||
return 'Année $annee';
|
||||
case ContributionType.trimestrielle:
|
||||
if (trimestre != null) {
|
||||
return 'T$trimestre $annee';
|
||||
}
|
||||
return 'Année $annee';
|
||||
case ContributionType.semestrielle:
|
||||
if (semestre != null) {
|
||||
return 'S$semestre $annee';
|
||||
}
|
||||
return 'Année $annee';
|
||||
case ContributionType.exceptionnelle:
|
||||
return 'Exceptionnelle $annee';
|
||||
}
|
||||
}
|
||||
|
||||
/// Nom du mois
|
||||
String _getNomMois(int mois) {
|
||||
const moisFr = [
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
||||
];
|
||||
if (mois >= 1 && mois <= 12) {
|
||||
return moisFr[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() =>
|
||||
'ContributionModel(id: $id, membre: $membreNomComplet, montant: $montant $devise, statut: $statut)';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contribution_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ContributionModel _$ContributionModelFromJson(Map<String, dynamic> json) =>
|
||||
ContributionModel(
|
||||
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(_$ContributionTypeEnumMap, json['type']) ??
|
||||
ContributionType.annuelle,
|
||||
statut:
|
||||
$enumDecodeNullable(_$ContributionStatusEnumMap, json['statut']) ??
|
||||
ContributionStatus.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(_$PaymentMethodEnumMap, 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> _$ContributionModelToJson(ContributionModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'membreId': instance.membreId,
|
||||
'membreNom': instance.membreNom,
|
||||
'membrePrenom': instance.membrePrenom,
|
||||
'organisationId': instance.organisationId,
|
||||
'organisationNom': instance.organisationNom,
|
||||
'type': _$ContributionTypeEnumMap[instance.type]!,
|
||||
'statut': _$ContributionStatusEnumMap[instance.statut]!,
|
||||
'montant': instance.montant,
|
||||
'montantPaye': instance.montantPaye,
|
||||
'devise': instance.devise,
|
||||
'dateEcheance': instance.dateEcheance.toIso8601String(),
|
||||
'datePaiement': instance.datePaiement?.toIso8601String(),
|
||||
'dateRappel': instance.dateRappel?.toIso8601String(),
|
||||
'methodePaiement': _$PaymentMethodEnumMap[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 _$ContributionTypeEnumMap = {
|
||||
ContributionType.annuelle: 'ANNUELLE',
|
||||
ContributionType.mensuelle: 'MENSUELLE',
|
||||
ContributionType.trimestrielle: 'TRIMESTRIELLE',
|
||||
ContributionType.semestrielle: 'SEMESTRIELLE',
|
||||
ContributionType.exceptionnelle: 'EXCEPTIONNELLE',
|
||||
};
|
||||
|
||||
const _$ContributionStatusEnumMap = {
|
||||
ContributionStatus.payee: 'PAYEE',
|
||||
ContributionStatus.nonPayee: 'NON_PAYEE',
|
||||
ContributionStatus.enRetard: 'EN_RETARD',
|
||||
ContributionStatus.partielle: 'PARTIELLE',
|
||||
ContributionStatus.annulee: 'ANNULEE',
|
||||
};
|
||||
|
||||
const _$PaymentMethodEnumMap = {
|
||||
PaymentMethod.especes: 'ESPECES',
|
||||
PaymentMethod.cheque: 'CHEQUE',
|
||||
PaymentMethod.virement: 'VIREMENT',
|
||||
PaymentMethod.carteBancaire: 'CARTE_BANCAIRE',
|
||||
PaymentMethod.waveMoney: 'WAVE_MONEY',
|
||||
PaymentMethod.orangeMoney: 'ORANGE_MONEY',
|
||||
PaymentMethod.freeMoney: 'FREE_MONEY',
|
||||
PaymentMethod.mobileMoney: 'MOBILE_MONEY',
|
||||
PaymentMethod.autre: 'AUTRE',
|
||||
};
|
||||
@@ -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/contributions_bloc.dart';
|
||||
|
||||
/// Enregistrer les dépendances du module Cotisations
|
||||
void registerCotisationsDependencies(GetIt getIt) {
|
||||
// BLoC
|
||||
getIt.registerFactory<ContributionsBloc>(
|
||||
() => ContributionsBloc(),
|
||||
);
|
||||
|
||||
// Repository sera ajouté ici quand l'API backend sera prête
|
||||
// getIt.registerLazySingleton<CotisationRepository>(
|
||||
// () => CotisationRepositoryImpl(dio: getIt()),
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
/// Page de gestion des contributions
|
||||
library contributions_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../shared/widgets/error_widget.dart';
|
||||
import '../../../../shared/widgets/loading_widget.dart';
|
||||
import '../../bloc/contributions_bloc.dart';
|
||||
import '../../bloc/contributions_event.dart';
|
||||
import '../../bloc/contributions_state.dart';
|
||||
import '../../data/models/contribution_model.dart';
|
||||
import 'package:unionflow_mobile_apps/features/contributions/presentation/widgets/create_contribution_dialog.dart';
|
||||
import '../widgets/payment_dialog.dart';
|
||||
import '../../../members/bloc/membres_bloc.dart';
|
||||
|
||||
/// Page principale des contributions
|
||||
class ContributionsPage extends StatefulWidget {
|
||||
const ContributionsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ContributionsPage> createState() => _ContributionsPageState();
|
||||
}
|
||||
|
||||
class _ContributionsPageState extends State<ContributionsPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final _currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: 'FCFA');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
_loadContributions();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadContributions() {
|
||||
final currentTab = _tabController.index;
|
||||
switch (currentTab) {
|
||||
case 0:
|
||||
context.read<ContributionsBloc>().add(const LoadContributions());
|
||||
break;
|
||||
case 1:
|
||||
context.read<ContributionsBloc>().add(const LoadContributionsPayees());
|
||||
break;
|
||||
case 2:
|
||||
context.read<ContributionsBloc>().add(const LoadContributionsNonPayees());
|
||||
break;
|
||||
case 3:
|
||||
context.read<ContributionsBloc>().add(const LoadContributionsEnRetard());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<ContributionsBloc, ContributionsState>(
|
||||
listener: (context, state) {
|
||||
// Gestion des erreurs avec SnackBar
|
||||
if (state is ContributionsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
textColor: Colors.white,
|
||||
onPressed: _loadContributions,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Cotisations'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
onTap: (_) => _loadContributions(),
|
||||
tabs: const [
|
||||
Tab(text: 'Toutes', icon: Icon(Icons.list)),
|
||||
Tab(text: 'Payées', icon: Icon(Icons.check_circle)),
|
||||
Tab(text: 'Non payées', icon: Icon(Icons.pending)),
|
||||
Tab(text: 'En retard', icon: Icon(Icons.warning)),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
onPressed: () => _showStats(),
|
||||
tooltip: 'Statistiques',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showCreateDialog(),
|
||||
tooltip: 'Nouvelle contribution',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildContributionsList(),
|
||||
_buildContributionsList(),
|
||||
_buildContributionsList(),
|
||||
_buildContributionsList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContributionsList() {
|
||||
return BlocBuilder<ContributionsBloc, ContributionsState>(
|
||||
builder: (context, state) {
|
||||
if (state is ContributionsLoading) {
|
||||
return const Center(child: AppLoadingWidget());
|
||||
}
|
||||
|
||||
if (state is ContributionsError) {
|
||||
return Center(
|
||||
child: AppErrorWidget(
|
||||
message: state.message,
|
||||
onRetry: _loadContributions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is ContributionsLoaded) {
|
||||
if (state.contributions.isEmpty) {
|
||||
return const Center(
|
||||
child: EmptyDataWidget(
|
||||
message: 'Aucune contribution trouvée',
|
||||
icon: Icons.payment,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => _loadContributions(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: state.contributions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contribution = state.contributions[index];
|
||||
return _buildContributionCard(contribution);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: Text('Chargez les cotisations'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContributionCard(ContributionModel contribution) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: InkWell(
|
||||
onTap: () => _showContributionDetails(contribution),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
contribution.membreNomComplet,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
contribution.libellePeriode,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatutChip(contribution.statut),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Montant',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_currencyFormat.format(contribution.montant),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (contribution.montantPaye != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Payé',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_currencyFormat.format(contribution.montantPaye),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'Échéance',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormat('dd/MM/yyyy').format(contribution.dateEcheance),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: contribution.estEnRetard ? Colors.red : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (contribution.statut == ContributionStatus.partielle)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: LinearProgressIndicator(
|
||||
value: contribution.pourcentagePaye / 100,
|
||||
backgroundColor: Colors.grey[200],
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutChip(ContributionStatus statut) {
|
||||
Color color;
|
||||
String label;
|
||||
IconData icon;
|
||||
|
||||
switch (statut) {
|
||||
case ContributionStatus.payee:
|
||||
color = Colors.green;
|
||||
label = 'Payée';
|
||||
icon = Icons.check_circle;
|
||||
break;
|
||||
case ContributionStatus.nonPayee:
|
||||
color = Colors.orange;
|
||||
label = 'Non payée';
|
||||
icon = Icons.pending;
|
||||
break;
|
||||
case ContributionStatus.enRetard:
|
||||
color = Colors.red;
|
||||
label = 'En retard';
|
||||
icon = Icons.warning;
|
||||
break;
|
||||
case ContributionStatus.partielle:
|
||||
color = Colors.blue;
|
||||
label = 'Partielle';
|
||||
icon = Icons.hourglass_bottom;
|
||||
break;
|
||||
case ContributionStatus.annulee:
|
||||
color = Colors.grey;
|
||||
label = 'Annulée';
|
||||
icon = Icons.cancel;
|
||||
break;
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(icon, size: 16, color: Colors.white),
|
||||
label: Text(label),
|
||||
backgroundColor: color,
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContributionDetails(ContributionModel contribution) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(contribution.membreNomComplet),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDetailRow('Période', contribution.libellePeriode),
|
||||
_buildDetailRow('Montant', _currencyFormat.format(contribution.montant)),
|
||||
if (contribution.montantPaye != null)
|
||||
_buildDetailRow('Payé', _currencyFormat.format(contribution.montantPaye)),
|
||||
_buildDetailRow('Restant', _currencyFormat.format(contribution.montantRestant)),
|
||||
_buildDetailRow(
|
||||
'Échéance',
|
||||
DateFormat('dd/MM/yyyy').format(contribution.dateEcheance),
|
||||
),
|
||||
if (contribution.datePaiement != null)
|
||||
_buildDetailRow(
|
||||
'Date paiement',
|
||||
DateFormat('dd/MM/yyyy').format(contribution.datePaiement!),
|
||||
),
|
||||
if (contribution.methodePaiement != null)
|
||||
_buildDetailRow('Méthode', _getMethodePaiementLabel(contribution.methodePaiement!)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (contribution.statut != ContributionStatus.payee)
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_showPaymentDialog(contribution);
|
||||
},
|
||||
icon: const Icon(Icons.payment),
|
||||
label: const Text('Enregistrer paiement'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMethodePaiementLabel(PaymentMethod methode) {
|
||||
switch (methode) {
|
||||
case PaymentMethod.especes:
|
||||
return 'Espèces';
|
||||
case PaymentMethod.cheque:
|
||||
return 'Chèque';
|
||||
case PaymentMethod.virement:
|
||||
return 'Virement';
|
||||
case PaymentMethod.carteBancaire:
|
||||
return 'Carte bancaire';
|
||||
case PaymentMethod.waveMoney:
|
||||
return 'Wave Money';
|
||||
case PaymentMethod.orangeMoney:
|
||||
return 'Orange Money';
|
||||
case PaymentMethod.freeMoney:
|
||||
return 'Free Money';
|
||||
case PaymentMethod.mobileMoney:
|
||||
return 'Mobile Money';
|
||||
case PaymentMethod.autre:
|
||||
return 'Autre';
|
||||
}
|
||||
}
|
||||
|
||||
void _showPaymentDialog(ContributionModel contribution) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: context.read<ContributionsBloc>(),
|
||||
child: PaymentDialog(cotisation: contribution),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: context.read<ContributionsBloc>()),
|
||||
BlocProvider.value(value: context.read<MembresBloc>()),
|
||||
],
|
||||
child: const CreateContributionDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showStats() {
|
||||
context.read<ContributionsBloc>().add(const LoadContributionsStats());
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Statistiques'),
|
||||
content: BlocBuilder<ContributionsBloc, ContributionsState>(
|
||||
builder: (context, state) {
|
||||
if (state is ContributionsStatsLoaded) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildStatRow('Total', state.stats['total'].toString()),
|
||||
_buildStatRow('Payées', state.stats['payees'].toString()),
|
||||
_buildStatRow('Non payées', state.stats['nonPayees'].toString()),
|
||||
_buildStatRow('En retard', state.stats['enRetard'].toString()),
|
||||
const Divider(),
|
||||
_buildStatRow(
|
||||
'Montant total',
|
||||
_currencyFormat.format(state.stats['montantTotal']),
|
||||
),
|
||||
_buildStatRow(
|
||||
'Montant payé',
|
||||
_currencyFormat.format(state.stats['montantPaye']),
|
||||
),
|
||||
_buildStatRow(
|
||||
'Taux recouvrement',
|
||||
'${state.stats['tauxRecouvrement'].toStringAsFixed(1)}%',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const AppLoadingWidget();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/// Wrapper BLoC pour la page des cotisations
|
||||
library cotisations_page_wrapper;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../bloc/contributions_bloc.dart';
|
||||
import '../../bloc/contributions_event.dart';
|
||||
import 'contributions_page.dart';
|
||||
|
||||
final _getIt = GetIt.instance;
|
||||
|
||||
/// Wrapper qui fournit le BLoC à la page des cotisations
|
||||
class CotisationsPageWrapper extends StatelessWidget {
|
||||
const CotisationsPageWrapper({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ContributionsBloc>(
|
||||
create: (context) {
|
||||
final bloc = _getIt<ContributionsBloc>();
|
||||
// Charger les cotisations au démarrage
|
||||
bloc.add(const LoadContributions());
|
||||
return bloc;
|
||||
},
|
||||
child: const ContributionsPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
/// Dialogue de création de contribution
|
||||
library create_contribution_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../bloc/contributions_bloc.dart';
|
||||
import '../../bloc/contributions_event.dart';
|
||||
import '../../data/models/contribution_model.dart';
|
||||
import '../../../members/bloc/membres_bloc.dart';
|
||||
import '../../../members/bloc/membres_event.dart';
|
||||
import '../../../members/bloc/membres_state.dart';
|
||||
|
||||
|
||||
class CreateContributionDialog extends StatefulWidget {
|
||||
const CreateContributionDialog({super.key});
|
||||
|
||||
@override
|
||||
State<CreateContributionDialog> createState() => _CreateContributionDialogState();
|
||||
}
|
||||
|
||||
class _CreateContributionDialogState extends State<CreateContributionDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _montantController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
ContributionType _selectedType = ContributionType.mensuelle;
|
||||
dynamic _selectedMembre;
|
||||
DateTime _dateEcheance = DateTime.now().add(const Duration(days: 30));
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Charger la liste des membres
|
||||
context.read<MembresBloc>().add(const LoadMembres());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_montantController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Nouvelle contribution'),
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Sélection du membre
|
||||
BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoaded) {
|
||||
return DropdownButtonFormField<dynamic>(
|
||||
value: _selectedMembre,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: state.membres.map((membre) {
|
||||
return DropdownMenuItem(
|
||||
value: membre,
|
||||
child: Text('${membre.nom} ${membre.prenom}'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedMembre = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return 'Veuillez sélectionner un membre';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
return const CircularProgressIndicator();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Type de contribution
|
||||
DropdownButtonFormField<ContributionType>(
|
||||
value: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type de contribution',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: ContributionType.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(_getTypeLabel(type)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Montant
|
||||
TextFormField(
|
||||
controller: _montantController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Montant (FCFA)',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez saisir un montant';
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Montant invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Date d'échéance
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateEcheance,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_dateEcheance = date;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Date d\'échéance',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
DateFormat('dd/MM/yyyy').format(_dateEcheance),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Description
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.description),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _createContribution,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Créer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getTypeLabel(ContributionType type) {
|
||||
switch (type) {
|
||||
case ContributionType.mensuelle:
|
||||
return 'Mensuelle';
|
||||
case ContributionType.trimestrielle:
|
||||
return 'Trimestrielle';
|
||||
case ContributionType.semestrielle:
|
||||
return 'Semestrielle';
|
||||
case ContributionType.annuelle:
|
||||
return 'Annuelle';
|
||||
case ContributionType.exceptionnelle:
|
||||
return 'Exceptionnelle';
|
||||
}
|
||||
}
|
||||
|
||||
void _createContribution() {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedMembre == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez sélectionner un membre'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
final contribution = ContributionModel(
|
||||
membreId: _selectedMembre!.id!,
|
||||
membreNom: _selectedMembre!.nom,
|
||||
membrePrenom: _selectedMembre!.prenom,
|
||||
type: _selectedType,
|
||||
annee: DateTime.now().year,
|
||||
montant: double.parse(_montantController.text),
|
||||
dateEcheance: _dateEcheance,
|
||||
description: _descriptionController.text.isNotEmpty ? _descriptionController.text : null,
|
||||
statut: ContributionStatus.nonPayee,
|
||||
dateCreation: DateTime.now(),
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
|
||||
context.read<ContributionsBloc>().add(CreateContribution(contribution: contribution));
|
||||
Navigator.pop(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Contribution créée avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
/// Dialogue de paiement de contribution
|
||||
/// Formulaire pour enregistrer un paiement de contribution
|
||||
library payment_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../bloc/contributions_bloc.dart';
|
||||
import '../../bloc/contributions_event.dart';
|
||||
import '../../data/models/contribution_model.dart';
|
||||
|
||||
/// Dialogue de paiement de contribution
|
||||
class PaymentDialog extends StatefulWidget {
|
||||
final ContributionModel cotisation;
|
||||
|
||||
const PaymentDialog({
|
||||
super.key,
|
||||
required this.cotisation,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentDialog> createState() => _PaymentDialogState();
|
||||
}
|
||||
|
||||
class _PaymentDialogState extends State<PaymentDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _montantController = TextEditingController();
|
||||
final _referenceController = TextEditingController();
|
||||
final _notesController = TextEditingController();
|
||||
|
||||
PaymentMethod _selectedMethode = PaymentMethod.waveMoney;
|
||||
DateTime _datePaiement = DateTime.now();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Pré-remplir avec le montant restant
|
||||
_montantController.text = widget.cotisation.montantRestant.toStringAsFixed(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_montantController.dispose();
|
||||
_referenceController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
constraints: const BoxConstraints(maxHeight: 500),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// En-tête
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF10B981),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(4),
|
||||
topRight: Radius.circular(4),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.payment, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Enregistrer un paiement',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Informations de la cotisation
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.cotisation.membreNomComplet,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.cotisation.libellePeriode,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Montant total:',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
Text(
|
||||
'${NumberFormat('#,###').format(widget.cotisation.montant)} ${widget.cotisation.devise}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Déjà payé:',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
Text(
|
||||
'${NumberFormat('#,###').format(widget.cotisation.montantPaye ?? 0)} ${widget.cotisation.devise}',
|
||||
style: const TextStyle(color: Colors.green),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Restant:',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
Text(
|
||||
'${NumberFormat('#,###').format(widget.cotisation.montantRestant)} ${widget.cotisation.devise}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Formulaire
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Montant
|
||||
TextFormField(
|
||||
controller: _montantController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Montant à payer *',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.attach_money),
|
||||
suffixText: widget.cotisation.devise,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Le montant est obligatoire';
|
||||
}
|
||||
final montant = double.tryParse(value);
|
||||
if (montant == null || montant <= 0) {
|
||||
return 'Montant invalide';
|
||||
}
|
||||
if (montant > widget.cotisation.montantRestant) {
|
||||
return 'Montant supérieur au restant dû';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Méthode de paiement
|
||||
DropdownButtonFormField<PaymentMethod>(
|
||||
value: _selectedMethode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Méthode de paiement *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.payment),
|
||||
),
|
||||
items: PaymentMethod.values.map((methode) {
|
||||
return DropdownMenuItem(
|
||||
value: methode,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(_getMethodeIcon(methode), size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(_getMethodeLabel(methode)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedMethode = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Date de paiement
|
||||
InkWell(
|
||||
onTap: () => _selectDate(context),
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Date de paiement *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
),
|
||||
child: Text(
|
||||
DateFormat('dd/MM/yyyy').format(_datePaiement),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Référence
|
||||
TextFormField(
|
||||
controller: _referenceController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Référence de transaction',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.receipt),
|
||||
hintText: 'Ex: TRX123456789',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Notes
|
||||
TextFormField(
|
||||
controller: _notesController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notes (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.note),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Boutons d'action
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _submitForm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Enregistrer le paiement'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getMethodeIcon(PaymentMethod methode) {
|
||||
switch (methode) {
|
||||
case PaymentMethod.waveMoney:
|
||||
return Icons.phone_android;
|
||||
case PaymentMethod.orangeMoney:
|
||||
return Icons.phone_iphone;
|
||||
case PaymentMethod.freeMoney:
|
||||
return Icons.smartphone;
|
||||
case PaymentMethod.mobileMoney:
|
||||
return Icons.mobile_friendly;
|
||||
case PaymentMethod.especes:
|
||||
return Icons.money;
|
||||
case PaymentMethod.cheque:
|
||||
return Icons.receipt_long;
|
||||
case PaymentMethod.virement:
|
||||
return Icons.account_balance;
|
||||
case PaymentMethod.carteBancaire:
|
||||
return Icons.credit_card;
|
||||
case PaymentMethod.autre:
|
||||
return Icons.more_horiz;
|
||||
}
|
||||
}
|
||||
|
||||
String _getMethodeLabel(PaymentMethod methode) {
|
||||
switch (methode) {
|
||||
case PaymentMethod.waveMoney:
|
||||
return 'Wave Money';
|
||||
case PaymentMethod.orangeMoney:
|
||||
return 'Orange Money';
|
||||
case PaymentMethod.freeMoney:
|
||||
return 'Free Money';
|
||||
case PaymentMethod.especes:
|
||||
return 'Espèces';
|
||||
case PaymentMethod.cheque:
|
||||
return 'Chèque';
|
||||
case PaymentMethod.virement:
|
||||
return 'Virement bancaire';
|
||||
case PaymentMethod.carteBancaire:
|
||||
return 'Carte bancaire';
|
||||
case PaymentMethod.mobileMoney:
|
||||
return 'Mobile Money (autre)';
|
||||
case PaymentMethod.autre:
|
||||
return 'Autre';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _datePaiement,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null && picked != _datePaiement) {
|
||||
setState(() {
|
||||
_datePaiement = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final montant = double.parse(_montantController.text);
|
||||
|
||||
// Créer la cotisation mise à jour
|
||||
widget.cotisation.copyWith(
|
||||
montantPaye: (widget.cotisation.montantPaye ?? 0) + montant,
|
||||
datePaiement: _datePaiement,
|
||||
methodePaiement: _selectedMethode,
|
||||
referencePaiement: _referenceController.text.isNotEmpty ? _referenceController.text : null,
|
||||
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
|
||||
statut: (widget.cotisation.montantPaye ?? 0) + montant >= widget.cotisation.montant
|
||||
? ContributionStatus.payee
|
||||
: ContributionStatus.partielle,
|
||||
);
|
||||
|
||||
// Envoyer l'événement au BLoC
|
||||
context.read<ContributionsBloc>().add(RecordPayment(
|
||||
contributionId: widget.cotisation.id!,
|
||||
montant: montant,
|
||||
methodePaiement: _selectedMethode,
|
||||
datePaiement: _datePaiement,
|
||||
reference: _referenceController.text.isNotEmpty ? _referenceController.text : null,
|
||||
notes: _notesController.text.isNotEmpty ? _notesController.text : null,
|
||||
));
|
||||
|
||||
// Fermer le dialogue
|
||||
Navigator.pop(context);
|
||||
|
||||
// Afficher un message de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Paiement enregistré avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user