/// BLoC pour la gestion des contributions library contributions_bloc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import '../../../core/utils/logger.dart'; import '../data/models/contribution_model.dart'; import '../domain/usecases/get_contributions.dart'; import '../domain/usecases/get_contribution_by_id.dart'; import '../domain/usecases/create_contribution.dart' as uc; import '../domain/usecases/update_contribution.dart' as uc; import '../domain/usecases/delete_contribution.dart' as uc; import '../domain/usecases/pay_contribution.dart'; import '../domain/usecases/get_contribution_stats.dart'; import '../domain/repositories/contribution_repository.dart'; import 'contributions_event.dart'; import 'contributions_state.dart'; /// BLoC pour gérer l'état des contributions via les use cases (Clean Architecture) @injectable class ContributionsBloc extends Bloc { final GetContributions _getContributions; final GetContributionById _getContributionById; final uc.CreateContribution _createContribution; final uc.UpdateContribution _updateContribution; final uc.DeleteContribution _deleteContribution; final PayContribution _payContribution; final GetContributionStats _getContributionStats; final IContributionRepository _repository; // Pour méthodes non-couvertes par use cases ContributionsBloc( this._getContributions, this._getContributionById, this._createContribution, this._updateContribution, this._deleteContribution, this._payContribution, this._getContributionStats, this._repository, ) : super(const ContributionsInitial()) { on(_onLoadContributions); on(_onLoadContributionById); on(_onCreateContribution); on(_onUpdateContribution); on(_onDeleteContribution); on(_onSearchContributions); on(_onLoadContributionsByMembre); on(_onLoadContributionsPayees); on(_onLoadContributionsNonPayees); on(_onLoadContributionsEnRetard); on(_onRecordPayment); on(_onLoadContributionsStats); on(_onGenerateAnnualContributions); on(_onSendPaymentReminder); } Future _onLoadContributions( LoadContributions event, Emitter emit, ) async { try { AppLogger.blocEvent('ContributionsBloc', 'LoadContributions', data: { 'page': event.page, 'size': event.size, }); emit(const ContributionsLoading(message: 'Chargement des contributions...')); // Use case: Get contributions final result = await _getContributions(page: event.page, size: event.size); emit(ContributionsLoaded( contributions: result.contributions, total: result.total, page: result.page, size: result.size, totalPages: result.totalPages, )); AppLogger.blocState('ContributionsBloc', 'ContributionsLoaded', data: { 'count': result.contributions.length, 'total': result.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)); } } Future _onLoadContributionById( LoadContributionById event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Chargement de la contribution...')); final contribution = await _getContributionById(event.id); emit(ContributionDetailLoaded(contribution: contribution)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Contribution non trouvée', error: e)); } } Future _onCreateContribution( CreateContribution event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Création de la contribution...')); final created = await _createContribution(event.contribution); emit(ContributionCreated(contribution: created)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur lors de la création de la contribution', error: e)); } } Future _onUpdateContribution( UpdateContribution event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Mise à jour de la contribution...')); final updated = await _updateContribution(event.id, event.contribution); emit(ContributionUpdated(contribution: updated)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur lors de la mise à jour', error: e)); } } Future _onDeleteContribution( DeleteContribution event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Suppression de la contribution...')); await _deleteContribution(event.id); emit(ContributionDeleted(id: event.id)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur lors de la suppression', error: e)); } } Future _onSearchContributions( SearchContributions event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Recherche en cours...')); final result = await _repository.getCotisations( page: event.page, size: event.size, membreId: event.membreId, statut: event.statut?.name, type: event.type?.name, annee: event.annee, ); emit(ContributionsLoaded( contributions: result.contributions, total: result.total, page: result.page, size: result.size, totalPages: result.totalPages, )); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur lors de la recherche', error: e)); } } Future _onLoadContributionsByMembre( LoadContributionsByMembre event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Chargement des contributions du membre...')); final result = await _repository.getCotisations( page: event.page, size: event.size, membreId: event.membreId, ); emit(ContributionsLoaded( contributions: result.contributions, total: result.total, page: result.page, size: result.size, totalPages: result.totalPages, )); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur lors du chargement', error: e)); } } Future _onLoadContributionsPayees( LoadContributionsPayees event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Chargement des contributions payées...')); final result = await _repository.getMesCotisations(); final payees = result.contributions.where((c) => c.statut == ContributionStatus.payee).toList(); emit(ContributionsLoaded( contributions: payees, total: payees.length, page: 0, size: payees.length, totalPages: payees.isEmpty ? 0 : 1, )); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur', error: e)); } } Future _onLoadContributionsNonPayees( LoadContributionsNonPayees event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Chargement des contributions non payées...')); final result = await _repository.getMesCotisations(); final nonPayees = result.contributions.where((c) => c.statut != ContributionStatus.payee).toList(); emit(ContributionsLoaded( contributions: nonPayees, total: nonPayees.length, page: 0, size: nonPayees.length, totalPages: nonPayees.isEmpty ? 0 : 1, )); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur', error: e)); } } Future _onLoadContributionsEnRetard( LoadContributionsEnRetard event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Chargement des contributions en retard...')); final result = await _repository.getMesCotisations(); final enRetard = result.contributions.where((c) => c.statut == ContributionStatus.enRetard || c.estEnRetard).toList(); emit(ContributionsLoaded( contributions: enRetard, total: enRetard.length, page: 0, size: enRetard.length, totalPages: enRetard.isEmpty ? 0 : 1, )); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur', error: e)); } } Future _onRecordPayment( RecordPayment event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Enregistrement du paiement...')); final updated = await _payContribution( cotisationId: event.contributionId, montant: event.montant, datePaiement: event.datePaiement, methodePaiement: event.methodePaiement.name, numeroPaiement: event.numeroPaiement, referencePaiement: event.referencePaiement, ); emit(PaymentRecorded(contribution: updated)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur lors de l\'enregistrement du paiement', error: e)); } } Future _onLoadContributionsStats( LoadContributionsStats event, Emitter emit, ) async { List? preservedList = state is ContributionsLoaded ? (state as ContributionsLoaded).contributions : null; try { // Charger synthèse + liste pour que la page « Mes statistiques » ait toujours donut et prochaines échéances final mesSynthese = await _getContributionStats(); final listResult = preservedList == null ? await _getContributions() : null; final contributions = preservedList ?? listResult?.contributions; if (mesSynthese != null && mesSynthese.isNotEmpty) { final normalized = _normalizeSyntheseForStats(mesSynthese); emit(ContributionsStatsLoaded(stats: normalized, contributions: contributions)); return; } final stats = await _repository.getStatistiques(); emit(ContributionsStatsLoaded( stats: stats.map((k, v) => MapEntry(k, v is num ? v.toDouble() : (v is int ? v.toDouble() : 0.0))), contributions: contributions, )); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur', error: e)); } } /// Normalise la réponse synthese (mes) pour l'affichage stats (clés numériques + isMesSynthese). Map _normalizeSyntheseForStats(Map s) { final montantDu = _toDouble(s['montantDu']); final totalPayeAnnee = _toDouble(s['totalPayeAnnee']); final totalAnnee = montantDu + totalPayeAnnee; final taux = totalAnnee > 0 ? (totalPayeAnnee / totalAnnee * 100) : 0.0; return { 'isMesSynthese': true, 'cotisationsEnAttente': (s['cotisationsEnAttente'] is int) ? s['cotisationsEnAttente'] as int : ((s['cotisationsEnAttente'] as num?)?.toInt() ?? 0), 'montantDu': montantDu, 'totalPayeAnnee': totalPayeAnnee, 'totalMontant': totalAnnee, 'tauxPaiement': taux, 'prochaineEcheance': s['prochaineEcheance']?.toString(), 'anneeEnCours': s['anneeEnCours'] is int ? s['anneeEnCours'] as int : ((s['anneeEnCours'] as num?)?.toInt() ?? DateTime.now().year), }; } double _toDouble(dynamic v) { if (v == null) return 0; if (v is num) return v.toDouble(); if (v is String) return double.tryParse(v) ?? 0; return 0; } Future _onGenerateAnnualContributions( GenerateAnnualContributions event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Génération des contributions...')); final count = await _repository.genererCotisationsAnnuelles(event.annee); emit(ContributionsGenerated(nombreGenere: count)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur', error: e)); } } Future _onSendPaymentReminder( SendPaymentReminder event, Emitter emit, ) async { try { emit(const ContributionsLoading(message: 'Envoi du rappel...')); await _repository.envoyerRappel(event.contributionId); emit(ReminderSent(contributionId: event.contributionId)); } catch (e, stackTrace) { AppLogger.error('Erreur', error: e, stackTrace: stackTrace); emit(ContributionsError(message: 'Erreur', error: e)); } } }