feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -2,17 +2,43 @@
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 '../data/repositories/contribution_repository.dart';
import '../data/repositories/contribution_repository.dart' show ContributionPageResult;
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 l'API backend
/// BLoC pour gérer l'état des contributions via les use cases (Clean Architecture)
@injectable
class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
final ContributionRepository _repository;
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._repository) : super(const ContributionsInitial()) {
ContributionsBloc(
this._getContributions,
this._getContributionById,
this._createContribution,
this._updateContribution,
this._deleteContribution,
this._payContribution,
this._getContributionStats,
this._repository,
) : super(const ContributionsInitial()) {
on<LoadContributions>(_onLoadContributions);
on<LoadContributionById>(_onLoadContributionById);
on<CreateContribution>(_onCreateContribution);
@@ -41,10 +67,8 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
emit(const ContributionsLoading(message: 'Chargement des contributions...'));
final result = await _repository.getCotisations(
page: event.page,
size: event.size,
);
// Use case: Get contributions
final result = await _getContributions(page: event.page, size: event.size);
emit(ContributionsLoaded(
contributions: result.contributions,
@@ -70,7 +94,7 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Chargement de la contribution...'));
final contribution = await _repository.getCotisationById(event.id);
final contribution = await _getContributionById(event.id);
emit(ContributionDetailLoaded(contribution: contribution));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
@@ -84,7 +108,7 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Création de la contribution...'));
final created = await _repository.createCotisation(event.contribution);
final created = await _createContribution(event.contribution);
emit(ContributionCreated(contribution: created));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
@@ -98,7 +122,7 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Mise à jour de la contribution...'));
final updated = await _repository.updateCotisation(event.id, event.contribution);
final updated = await _updateContribution(event.id, event.contribution);
emit(ContributionUpdated(contribution: updated));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
@@ -112,7 +136,7 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Suppression de la contribution...'));
await _repository.deleteCotisation(event.id);
await _deleteContribution(event.id);
emit(ContributionDeleted(id: event.id));
} catch (e, stackTrace) {
AppLogger.error('Erreur', error: e, stackTrace: stackTrace);
@@ -181,19 +205,14 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Chargement des contributions payées...'));
final result = await _repository.getCotisations(
page: event.page,
size: event.size,
statut: 'PAYEE',
);
final result = await _repository.getMesCotisations();
final payees = result.contributions.where((c) => c.statut == ContributionStatus.payee).toList();
emit(ContributionsLoaded(
contributions: result.contributions,
total: result.total,
page: result.page,
size: result.size,
totalPages: result.totalPages,
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);
@@ -207,19 +226,14 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Chargement des contributions non payées...'));
final result = await _repository.getCotisations(
page: event.page,
size: event.size,
statut: 'NON_PAYEE',
);
final result = await _repository.getMesCotisations();
final nonPayees = result.contributions.where((c) => c.statut != ContributionStatus.payee).toList();
emit(ContributionsLoaded(
contributions: result.contributions,
total: result.total,
page: result.page,
size: result.size,
totalPages: result.totalPages,
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);
@@ -233,19 +247,14 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
) async {
try {
emit(const ContributionsLoading(message: 'Chargement des contributions en retard...'));
final result = await _repository.getCotisations(
page: event.page,
size: event.size,
statut: 'EN_RETARD',
);
final result = await _repository.getMesCotisations();
final enRetard = result.contributions.where((c) => c.statut == ContributionStatus.enRetard || c.estEnRetard).toList();
emit(ContributionsLoaded(
contributions: result.contributions,
total: result.total,
page: result.page,
size: result.size,
totalPages: result.totalPages,
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);
@@ -260,8 +269,8 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
try {
emit(const ContributionsLoading(message: 'Enregistrement du paiement...'));
final updated = await _repository.enregistrerPaiement(
event.contributionId,
final updated = await _payContribution(
cotisationId: event.contributionId,
montant: event.montant,
datePaiement: event.datePaiement,
methodePaiement: event.methodePaiement.name,
@@ -280,16 +289,54 @@ class ContributionsBloc extends Bloc<ContributionsEvent, ContributionsState> {
LoadContributionsStats event,
Emitter<ContributionsState> emit,
) async {
List<ContributionModel>? preservedList = state is ContributionsLoaded ? (state as ContributionsLoaded).contributions : null;
try {
emit(const ContributionsLoading(message: 'Chargement des statistiques...'));
// 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() : 0.0))));
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<String, dynamic> _normalizeSyntheseForStats(Map<String, dynamic> 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<void> _onGenerateAnnualContributions(
GenerateAnnualContributions event,
Emitter<ContributionsState> emit,