/// 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/repositories/organization_repository.dart'; import 'organizations_event.dart'; import 'organizations_state.dart'; /// BLoC principal pour la gestion des organisations @injectable class OrganizationsBloc extends Bloc { final GetOrganizations _getOrganizations; final GetOrganizationById _getOrganizationById; final uc.CreateOrganization _createOrganization; final uc.UpdateOrganization _updateOrganization; final uc.DeleteOrganization _deleteOrganization; final IOrganizationRepository _repository; // Pour méthodes non-couvertes (activate, suspend, search, stats) final OrganizationService _organizationService; // Pour helpers (sort, filter local) OrganizationsBloc( this._getOrganizations, this._getOrganizationById, this._createOrganization, this._updateOrganization, this._deleteOrganization, this._repository, this._organizationService, ) : super(const OrganizationsInitial()) { // Enregistrement des handlers d'événements on(_onLoadOrganizations); on(_onLoadMoreOrganizations); on(_onSearchOrganizations); on(_onAdvancedSearchOrganizations); on(_onLoadOrganizationById); on(_onCreateOrganization); on(_onUpdateOrganization); on(_onDeleteOrganization); on(_onActivateOrganization); on(_onSuspendOrganization); on(_onFilterOrganizationsByStatus); on(_onFilterOrganizationsByType); on(_onSortOrganizations); on(_onLoadOrganizationsStats); on(_onClearOrganizationsFilters); on(_onRefreshOrganizations); on(_onResetOrganizationsState); } /// Charge la liste des organisations Future _onLoadOrganizations( LoadOrganizations event, Emitter emit, ) async { try { if (event.refresh || state is! OrganizationsLoaded) { emit(const OrganizationsLoading()); } List listToUse; List? 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().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: 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( 'Erreur lors du chargement des organisations', details: e.toString(), )); } } /// Charge plus d'organisations (pagination) Future _onLoadMoreOrganizations( LoadMoreOrganizations event, Emitter emit, ) async { final currentState = state; if (currentState is! OrganizationsLoaded || currentState.hasReachedMax) { return; } emit(OrganizationsLoadingMore(currentState.organizations)); try { final nextPage = currentState.currentPage + 1; final newOrganizations = await _getOrganizations( page: nextPage, size: 20, recherche: currentState.currentSearch, ); 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( organizations: allOrganizations, filteredOrganizations: filteredOrganizations, hasReachedMax: newOrganizations.length < 20, currentPage: nextPage, )); } catch (e) { emit(OrganizationsError( 'Erreur lors du chargement de plus d\'organisations', details: e.toString(), previousOrganizations: currentState.organizations, )); } } /// Recherche des organisations Future _onSearchOrganizations( SearchOrganizations event, Emitter emit, ) async { final currentState = state; if (currentState is! OrganizationsLoaded) { // Si pas encore chargé, charger avec recherche add(LoadOrganizations(recherche: event.query, refresh: true)); return; } try { if (event.query.isEmpty) { // Recherche vide, afficher toutes les organisations final filteredOrganizations = _applyCurrentFilters( currentState.organizations, currentState.copyWith(clearSearch: true), ); emit(currentState.copyWith( filteredOrganizations: filteredOrganizations, clearSearch: true, )); } else { // Recherche locale d'abord final localResults = _organizationService.searchLocal( currentState.organizations, event.query, ); emit(currentState.copyWith( filteredOrganizations: localResults, currentSearch: event.query, )); // Puis recherche serveur pour plus de résultats final serverResults = await _getOrganizations( page: 0, size: 50, recherche: event.query, ); final filteredResults = _applyCurrentFilters(serverResults, currentState); emit(currentState.copyWith( organizations: serverResults, filteredOrganizations: filteredResults, currentSearch: event.query, currentPage: 0, hasReachedMax: true, )); } } catch (e) { emit(OrganizationsError( 'Erreur lors de la recherche', details: e.toString(), previousOrganizations: currentState.organizations, )); } } /// Recherche avancée Future _onAdvancedSearchOrganizations( AdvancedSearchOrganizations event, Emitter emit, ) async { emit(const OrganizationsLoading()); try { final organizations = await _repository.searchOrganizations( nom: event.nom, type: event.type, statut: event.statut, ville: event.ville, region: event.region, pays: event.pays, page: event.page, size: event.size, ); emit(OrganizationsLoaded( organizations: organizations, filteredOrganizations: organizations, hasReachedMax: organizations.length < event.size, currentPage: event.page, typeFilter: event.type, statusFilter: event.statut, )); } catch (e) { emit(OrganizationsError( 'Erreur lors de la recherche avancée', details: e.toString(), )); } } /// Charge une organisation par ID Future _onLoadOrganizationById( LoadOrganizationById event, Emitter emit, ) async { emit(OrganizationLoading(event.id)); try { final organization = await _getOrganizationById(event.id); if (organization != null) { emit(OrganizationLoaded(organization)); } else { emit(OrganizationError('Organisation non trouvée', organizationId: event.id)); } } catch (e) { emit(OrganizationError( 'Erreur lors du chargement de l\'organisation', organizationId: event.id, )); } } /// Crée une nouvelle organisation Future _onCreateOrganization( CreateOrganization event, Emitter emit, ) async { emit(const OrganizationCreating()); try { final createdOrganization = await _createOrganization(event.organization); emit(OrganizationCreated(createdOrganization)); // Recharger la liste si elle était déjà chargée if (state is OrganizationsLoaded) { add(const RefreshOrganizations()); } } catch (e) { emit(OrganizationsError( 'Erreur lors de la création de l\'organisation', details: e.toString(), )); } } /// Met à jour une organisation Future _onUpdateOrganization( UpdateOrganization event, Emitter emit, ) async { emit(OrganizationUpdating(event.id)); try { final updatedOrganization = await _updateOrganization( event.id, event.organization, ); emit(OrganizationUpdated(updatedOrganization)); // Mettre à jour la liste si elle était déjà chargée final currentState = state; if (currentState is OrganizationsLoaded) { final updatedList = currentState.organizations.map((org) { return org.id == event.id ? updatedOrganization : org; }).toList(); final filteredList = _applyCurrentFilters(updatedList, currentState); emit(currentState.copyWith( organizations: updatedList, filteredOrganizations: filteredList, )); } } catch (e) { emit(OrganizationsError( 'Erreur lors de la mise à jour de l\'organisation', details: e.toString(), )); } } /// Supprime une organisation Future _onDeleteOrganization( DeleteOrganization event, Emitter emit, ) async { emit(OrganizationDeleting(event.id)); try { await _deleteOrganization(event.id); emit(OrganizationDeleted(event.id)); // Retirer de la liste si elle était déjà chargée final currentState = state; if (currentState is OrganizationsLoaded) { final updatedList = currentState.organizations.where((org) => org.id != event.id).toList(); final filteredList = _applyCurrentFilters(updatedList, currentState); emit(currentState.copyWith( organizations: updatedList, filteredOrganizations: filteredList, )); } } catch (e) { emit(OrganizationsError( 'Erreur lors de la suppression de l\'organisation', details: e.toString(), )); } } /// Active une organisation Future _onActivateOrganization( ActivateOrganization event, Emitter emit, ) async { emit(OrganizationActivating(event.id)); try { final activatedOrganization = await _repository.activateOrganization(event.id); emit(OrganizationActivated(activatedOrganization)); // Mettre à jour la liste si elle était déjà chargée final currentState = state; if (currentState is OrganizationsLoaded) { final updatedList = currentState.organizations.map((org) { return org.id == event.id ? activatedOrganization : org; }).toList(); final filteredList = _applyCurrentFilters(updatedList, currentState); emit(currentState.copyWith( organizations: updatedList, filteredOrganizations: filteredList, )); } } catch (e) { emit(OrganizationsError( 'Erreur lors de l\'activation de l\'organisation', details: e.toString(), )); } } /// Suspend une organisation Future _onSuspendOrganization( SuspendOrganization event, Emitter emit, ) async { emit(OrganizationSuspending(event.id)); try { final suspendedOrganization = await _repository.suspendOrganization(event.id); emit(OrganizationSuspended(suspendedOrganization)); // Mettre à jour la liste si elle était déjà chargée final currentState = state; if (currentState is OrganizationsLoaded) { final updatedList = currentState.organizations.map((org) { return org.id == event.id ? suspendedOrganization : org; }).toList(); final filteredList = _applyCurrentFilters(updatedList, currentState); emit(currentState.copyWith( organizations: updatedList, filteredOrganizations: filteredList, )); } } catch (e) { emit(OrganizationsError( 'Erreur lors de la suspension de l\'organisation', details: e.toString(), )); } } /// Filtre par statut void _onFilterOrganizationsByStatus( FilterOrganizationsByStatus event, Emitter emit, ) { final currentState = state; if (currentState is! OrganizationsLoaded) return; final filteredOrganizations = _applyCurrentFilters( currentState.organizations, currentState.copyWith(statusFilter: event.statut), ); emit(currentState.copyWith( filteredOrganizations: filteredOrganizations, statusFilter: event.statut, )); } /// Filtre par type void _onFilterOrganizationsByType( FilterOrganizationsByType event, Emitter emit, ) { final currentState = state; if (currentState is! OrganizationsLoaded) return; final filteredOrganizations = _applyCurrentFilters( currentState.organizations, currentState.copyWith(typeFilter: event.type), ); emit(currentState.copyWith( filteredOrganizations: filteredOrganizations, typeFilter: event.type, )); } /// Trie les organisations void _onSortOrganizations( SortOrganizations event, Emitter emit, ) { final currentState = state; if (currentState is! OrganizationsLoaded) return; List sortedOrganizations; switch (event.sortType) { case OrganizationSortType.name: sortedOrganizations = _organizationService.sortByName( currentState.filteredOrganizations, ascending: event.ascending, ); break; case OrganizationSortType.creationDate: sortedOrganizations = _organizationService.sortByCreationDate( currentState.filteredOrganizations, ascending: event.ascending, ); break; case OrganizationSortType.memberCount: sortedOrganizations = _organizationService.sortByMemberCount( currentState.filteredOrganizations, ascending: event.ascending, ); break; default: sortedOrganizations = currentState.filteredOrganizations; } emit(currentState.copyWith( filteredOrganizations: sortedOrganizations, sortType: event.sortType, sortAscending: event.ascending, )); } /// Charge les statistiques Future _onLoadOrganizationsStats( LoadOrganizationsStats event, Emitter emit, ) async { emit(const OrganizationsStatsLoading()); try { final stats = await _repository.getOrganizationsStats(); emit(OrganizationsStatsLoaded(stats)); } catch (e) { emit(const OrganizationsStatsError('Erreur lors du chargement des statistiques')); } } /// Efface les filtres void _onClearOrganizationsFilters( ClearOrganizationsFilters event, Emitter emit, ) { final currentState = state; if (currentState is! OrganizationsLoaded) return; emit(currentState.copyWith( filteredOrganizations: currentState.organizations, clearSearch: true, clearStatusFilter: true, clearTypeFilter: true, clearSort: true, )); } /// Rafraîchit les données void _onRefreshOrganizations( RefreshOrganizations event, Emitter emit, ) { 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 void _onResetOrganizationsState( ResetOrganizationsState event, Emitter emit, ) { emit(const OrganizationsInitial()); } /// Applique les filtres actuels à une liste d'organisations List _applyCurrentFilters( List organizations, OrganizationsLoaded state, ) { var filtered = organizations; // Filtre par recherche if (state.currentSearch?.isNotEmpty == true) { filtered = _organizationService.searchLocal(filtered, state.currentSearch!); } // Filtre par statut if (state.statusFilter != null) { filtered = _organizationService.filterByStatus(filtered, state.statusFilter!); } // Filtre par type if (state.typeFilter != null) { filtered = _organizationService.filterByType(filtered, state.typeFilter!); } return filtered; } }