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:
@@ -1,17 +1,45 @@
|
||||
/// BLoC pour la gestion des organisations
|
||||
/// BLoC pour la gestion des organisations (Clean Architecture)
|
||||
library organizations_bloc;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../data/models/organization_model.dart';
|
||||
import '../data/services/organization_service.dart';
|
||||
import '../domain/usecases/get_organizations.dart';
|
||||
import '../domain/usecases/get_organization_by_id.dart';
|
||||
import '../domain/usecases/create_organization.dart' as uc;
|
||||
import '../domain/usecases/update_organization.dart' as uc;
|
||||
import '../domain/usecases/delete_organization.dart' as uc;
|
||||
import '../domain/usecases/get_organization_members.dart';
|
||||
import '../domain/usecases/update_organization_config.dart';
|
||||
import '../domain/repositories/organization_repository.dart';
|
||||
import 'organizations_event.dart';
|
||||
import 'organizations_state.dart';
|
||||
|
||||
/// BLoC principal pour la gestion des organisations
|
||||
@injectable
|
||||
class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
final OrganizationService _organizationService;
|
||||
final GetOrganizations _getOrganizations;
|
||||
final GetOrganizationById _getOrganizationById;
|
||||
final uc.CreateOrganization _createOrganization;
|
||||
final uc.UpdateOrganization _updateOrganization;
|
||||
final uc.DeleteOrganization _deleteOrganization;
|
||||
final GetOrganizationMembers _getOrganizationMembers;
|
||||
final UpdateOrganizationConfig _updateOrganizationConfig;
|
||||
final IOrganizationRepository _repository; // Pour méthodes non-couvertes (activate, suspend, search, stats)
|
||||
final OrganizationService _organizationService; // Pour helpers (sort, filter local)
|
||||
|
||||
OrganizationsBloc(this._organizationService) : super(const OrganizationsInitial()) {
|
||||
OrganizationsBloc(
|
||||
this._getOrganizations,
|
||||
this._getOrganizationById,
|
||||
this._createOrganization,
|
||||
this._updateOrganization,
|
||||
this._deleteOrganization,
|
||||
this._getOrganizationMembers,
|
||||
this._updateOrganizationConfig,
|
||||
this._repository,
|
||||
this._organizationService,
|
||||
) : super(const OrganizationsInitial()) {
|
||||
// Enregistrement des handlers d'événements
|
||||
on<LoadOrganizations>(_onLoadOrganizations);
|
||||
on<LoadMoreOrganizations>(_onLoadMoreOrganizations);
|
||||
@@ -42,18 +70,39 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationsLoading());
|
||||
}
|
||||
|
||||
final organizations = await _organizationService.getOrganizations(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
recherche: event.recherche,
|
||||
);
|
||||
List<OrganizationModel> listToUse;
|
||||
List<String>? filterIds;
|
||||
if (event.useMesOnly) {
|
||||
// Admin d'organisation : endpoint backend /mes retourne uniquement ses organisations
|
||||
listToUse = await _repository.getMesOrganisations();
|
||||
filterIds = listToUse.map((o) => o.id).whereType<String>().toList();
|
||||
} else {
|
||||
final organizations = await _getOrganizations(
|
||||
page: event.page,
|
||||
size: event.size,
|
||||
recherche: event.recherche,
|
||||
);
|
||||
if (event.filterOrganizationIds != null &&
|
||||
event.filterOrganizationIds!.isNotEmpty) {
|
||||
final allowedIds = event.filterOrganizationIds!.toSet();
|
||||
listToUse = organizations
|
||||
.where((org) => org.id != null && allowedIds.contains(org.id))
|
||||
.toList();
|
||||
filterIds = event.filterOrganizationIds;
|
||||
} else {
|
||||
listToUse = organizations;
|
||||
filterIds = null;
|
||||
}
|
||||
}
|
||||
|
||||
emit(OrganizationsLoaded(
|
||||
organizations: organizations,
|
||||
filteredOrganizations: organizations,
|
||||
hasReachedMax: organizations.length < event.size,
|
||||
organizations: listToUse,
|
||||
filteredOrganizations: listToUse,
|
||||
hasReachedMax: event.useMesOnly || listToUse.length < event.size,
|
||||
currentPage: event.page,
|
||||
currentSearch: event.recherche,
|
||||
filterOrganizationIds: filterIds,
|
||||
useMesOnly: event.useMesOnly,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(OrganizationsError(
|
||||
@@ -77,13 +126,21 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
|
||||
try {
|
||||
final nextPage = currentState.currentPage + 1;
|
||||
final newOrganizations = await _organizationService.getOrganizations(
|
||||
final newOrganizations = await _getOrganizations(
|
||||
page: nextPage,
|
||||
size: 20,
|
||||
recherche: currentState.currentSearch,
|
||||
);
|
||||
|
||||
final allOrganizations = [...currentState.organizations, ...newOrganizations];
|
||||
var allOrganizations = [...currentState.organizations, ...newOrganizations];
|
||||
// Réappliquer le filtre orgAdmin si présent
|
||||
if (currentState.filterOrganizationIds != null &&
|
||||
currentState.filterOrganizationIds!.isNotEmpty) {
|
||||
final allowedIds = currentState.filterOrganizationIds!.toSet();
|
||||
allOrganizations = allOrganizations
|
||||
.where((org) => org.id != null && allowedIds.contains(org.id))
|
||||
.toList();
|
||||
}
|
||||
final filteredOrganizations = _applyCurrentFilters(allOrganizations, currentState);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
@@ -137,7 +194,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
));
|
||||
|
||||
// Puis recherche serveur pour plus de résultats
|
||||
final serverResults = await _organizationService.getOrganizations(
|
||||
final serverResults = await _getOrganizations(
|
||||
page: 0,
|
||||
size: 50,
|
||||
recherche: event.query,
|
||||
@@ -169,7 +226,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationsLoading());
|
||||
|
||||
try {
|
||||
final organizations = await _organizationService.searchOrganizations(
|
||||
final organizations = await _repository.searchOrganizations(
|
||||
nom: event.nom,
|
||||
type: event.type,
|
||||
statut: event.statut,
|
||||
@@ -204,7 +261,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationLoading(event.id));
|
||||
|
||||
try {
|
||||
final organization = await _organizationService.getOrganizationById(event.id);
|
||||
final organization = await _getOrganizationById(event.id);
|
||||
if (organization != null) {
|
||||
emit(OrganizationLoaded(organization));
|
||||
} else {
|
||||
@@ -226,7 +283,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationCreating());
|
||||
|
||||
try {
|
||||
final createdOrganization = await _organizationService.createOrganization(event.organization);
|
||||
final createdOrganization = await _createOrganization(event.organization);
|
||||
emit(OrganizationCreated(createdOrganization));
|
||||
|
||||
// Recharger la liste si elle était déjà chargée
|
||||
@@ -249,7 +306,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationUpdating(event.id));
|
||||
|
||||
try {
|
||||
final updatedOrganization = await _organizationService.updateOrganization(
|
||||
final updatedOrganization = await _updateOrganization(
|
||||
event.id,
|
||||
event.organization,
|
||||
);
|
||||
@@ -284,7 +341,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationDeleting(event.id));
|
||||
|
||||
try {
|
||||
await _organizationService.deleteOrganization(event.id);
|
||||
await _deleteOrganization(event.id);
|
||||
emit(OrganizationDeleted(event.id));
|
||||
|
||||
// Retirer de la liste si elle était déjà chargée
|
||||
@@ -313,7 +370,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationActivating(event.id));
|
||||
|
||||
try {
|
||||
final activatedOrganization = await _organizationService.activateOrganization(event.id);
|
||||
final activatedOrganization = await _repository.activateOrganization(event.id);
|
||||
emit(OrganizationActivated(activatedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
@@ -345,7 +402,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationSuspending(event.id));
|
||||
|
||||
try {
|
||||
final suspendedOrganization = await _organizationService.suspendOrganization(event.id);
|
||||
final suspendedOrganization = await _repository.suspendOrganization(event.id);
|
||||
emit(OrganizationSuspended(suspendedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
@@ -454,7 +511,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(const OrganizationsStatsLoading());
|
||||
|
||||
try {
|
||||
final stats = await _organizationService.getOrganizationsStats();
|
||||
final stats = await _repository.getOrganizationsStats();
|
||||
emit(OrganizationsStatsLoaded(stats));
|
||||
} catch (e) {
|
||||
emit(const OrganizationsStatsError('Erreur lors du chargement des statistiques'));
|
||||
@@ -483,7 +540,15 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
RefreshOrganizations event,
|
||||
Emitter<OrganizationsState> emit,
|
||||
) {
|
||||
add(const LoadOrganizations(refresh: true));
|
||||
final currentState = state;
|
||||
if (currentState is OrganizationsLoaded && currentState.useMesOnly) {
|
||||
add(const LoadOrganizations(refresh: true, useMesOnly: true));
|
||||
} else {
|
||||
final filterIds = currentState is OrganizationsLoaded
|
||||
? currentState.filterOrganizationIds
|
||||
: null;
|
||||
add(LoadOrganizations(refresh: true, filterOrganizationIds: filterIds));
|
||||
}
|
||||
}
|
||||
|
||||
/// Remet à zéro l'état
|
||||
|
||||
@@ -18,16 +18,22 @@ class LoadOrganizations extends OrganizationsEvent {
|
||||
final int size;
|
||||
final String? recherche;
|
||||
final bool refresh;
|
||||
/// Filtre par IDs d'organisations (ex. pour orgAdmin : uniquement son organisation)
|
||||
final List<String>? filterOrganizationIds;
|
||||
/// Si true, appelle GET /api/organisations/mes (uniquement les orgs du membre connecté)
|
||||
final bool useMesOnly;
|
||||
|
||||
const LoadOrganizations({
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.recherche,
|
||||
this.refresh = false,
|
||||
this.filterOrganizationIds,
|
||||
this.useMesOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, recherche, refresh];
|
||||
List<Object?> get props => [page, size, recherche, refresh, filterOrganizationIds, useMesOnly];
|
||||
}
|
||||
|
||||
/// Événement pour charger plus d'organisations (pagination)
|
||||
|
||||
@@ -44,6 +44,10 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
final OrganizationSortType? sortType;
|
||||
final bool sortAscending;
|
||||
final Map<String, dynamic>? stats;
|
||||
/// Filtre appliqué (ex. orgAdmin : uniquement ses organisations)
|
||||
final List<String>? filterOrganizationIds;
|
||||
/// True si la liste provient de GET /mes (admin d'organisation)
|
||||
final bool useMesOnly;
|
||||
|
||||
const OrganizationsLoaded({
|
||||
required this.organizations,
|
||||
@@ -56,6 +60,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
this.sortType,
|
||||
this.sortAscending = true,
|
||||
this.stats,
|
||||
this.filterOrganizationIds,
|
||||
this.useMesOnly = false,
|
||||
});
|
||||
|
||||
/// Copie avec modifications
|
||||
@@ -70,6 +76,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
OrganizationSortType? sortType,
|
||||
bool? sortAscending,
|
||||
Map<String, dynamic>? stats,
|
||||
List<String>? filterOrganizationIds,
|
||||
bool? useMesOnly,
|
||||
bool clearSearch = false,
|
||||
bool clearStatusFilter = false,
|
||||
bool clearTypeFilter = false,
|
||||
@@ -86,6 +94,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
sortType: clearSort ? null : (sortType ?? this.sortType),
|
||||
sortAscending: sortAscending ?? this.sortAscending,
|
||||
stats: stats ?? this.stats,
|
||||
filterOrganizationIds: filterOrganizationIds ?? this.filterOrganizationIds,
|
||||
useMesOnly: useMesOnly ?? this.useMesOnly,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,6 +140,8 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
sortType,
|
||||
sortAscending,
|
||||
stats,
|
||||
filterOrganizationIds,
|
||||
useMesOnly,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user