/// BLoC pour la gestion des membres library membres_bloc; import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dio/dio.dart'; import 'package:injectable/injectable.dart'; import 'membres_event.dart'; import 'membres_state.dart'; import '../../../shared/models/membre_search_criteria.dart'; import '../../../shared/models/membre_search_result.dart'; import '../domain/usecases/get_members.dart'; import '../domain/usecases/get_member_by_id.dart'; import '../domain/usecases/create_member.dart' as uc; import '../domain/usecases/update_member.dart' as uc; import '../domain/usecases/delete_member.dart' as uc; import '../domain/usecases/search_members.dart'; import '../domain/usecases/get_member_stats.dart'; import '../domain/repositories/membre_repository.dart'; import '../../../core/websocket/websocket_service.dart'; import '../../../core/utils/logger.dart'; /// BLoC pour la gestion des membres (Clean Architecture) @injectable class MembresBloc extends Bloc { final GetMembers _getMembers; final GetMemberById _getMemberById; final uc.CreateMember _createMember; final uc.UpdateMember _updateMember; final uc.DeleteMember _deleteMember; final SearchMembers _searchMembers; final GetMemberStats _getMemberStats; final IMembreRepository _repository; final WebSocketService _webSocketService; StreamSubscription? _webSocketSubscription; MembresBloc( this._getMembers, this._getMemberById, this._createMember, this._updateMember, this._deleteMember, this._searchMembers, this._getMemberStats, this._repository, this._webSocketService, ) : super(const MembresInitial()) { on(_onLoadMembres); on(_onLoadMembreById); on(_onCreateMembre); on(_onUpdateMembre); on(_onDeleteMembre); on(_onActivateMembre); on(_onDeactivateMembre); on(_onSearchMembres); on(_onLoadActiveMembres); on(_onLoadBureauMembres); on(_onLoadMembresStats); on(_onResetMotDePasse); on(_onAffecterOrganisation); on(_onInviterMembre); on(_onActiverAdhesion); on(_onSuspendrAdhesion); on(_onRadierAdhesion); _initWebSocketListener(); } void _initWebSocketListener() { _webSocketSubscription = _webSocketService.eventStream.listen( (event) { try { if (event is MemberEvent) { AppLogger.info('MembresBloc: MemberEvent reçu (${event.eventType}), refresh liste'); final currentState = state; if (currentState is MembresLoaded && !isClosed) { add(LoadMembres( refresh: true, organisationId: currentState.organisationId, page: currentState.currentPage, size: currentState.pageSize, )); } } } catch (e, s) { AppLogger.error('MembresBloc: erreur lors du traitement WebSocket event', error: e); } }, onError: (error) => AppLogger.error('MembresBloc: WebSocket stream error', error: error), ); } @override Future close() { _webSocketSubscription?.cancel(); return super.close(); } /// Charge la liste des membres Future _onLoadMembres( LoadMembres event, Emitter emit, ) async { try { // Si refresh et qu'on a déjà des données, on garde l'état actuel if (event.refresh && state is MembresLoaded) { final currentState = state as MembresLoaded; emit(MembresRefreshing(currentState.membres)); } else { emit(const MembresLoading()); } final MembreSearchResult result; if (event.organisationId != null) { // OrgAdmin : scope la requête à son organisation via la recherche avancée. // includeInactifs=true pour récupérer aussi les membres "En attente" // (actif=false) — le filtrage par statut est géré côté UI. result = await _searchMembers( criteria: MembreSearchCriteria( organisationIds: [event.organisationId!], query: event.recherche?.isNotEmpty == true ? event.recherche : null, includeInactifs: true, ), page: event.page, size: event.size, ); } else { // SuperAdmin et autres rôles : accès global sans filtre org result = await _getMembers( page: event.page, size: event.size, recherche: event.recherche, ); } emit(MembresLoaded( membres: result.membres, totalElements: result.totalElements, currentPage: result.currentPage, pageSize: result.pageSize, totalPages: result.totalPages, organisationId: event.organisationId, )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors du chargement des membres. Veuillez réessayer.', error: e, )); } } /// Charge un membre par ID Future _onLoadMembreById( LoadMembreById event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _getMemberById(event.id); if (membre != null) { emit(MembreDetailLoaded(membre)); } else { emit(const MembresError( message: 'Membre non trouvé', code: '404', )); } } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors du chargement du membre. Veuillez réessayer.', error: e, )); } } /// Crée un nouveau membre Future _onCreateMembre( CreateMembre event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _createMember(event.membre); emit(MembreCreated(membre)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; if (e.response?.statusCode == 400) { // Erreur de validation final errors = _extractValidationErrors(e.response?.data); emit(MembresValidationError( message: 'Erreur de validation', validationErrors: errors, code: '400', )); } else { emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de la création du membre. Veuillez réessayer.', error: e, )); } } /// Met à jour un membre Future _onUpdateMembre( UpdateMembre event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _updateMember(event.id, event.membre); emit(MembreUpdated(membre)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; if (e.response?.statusCode == 400) { final errors = _extractValidationErrors(e.response?.data); emit(MembresValidationError( message: 'Erreur de validation', validationErrors: errors, code: '400', )); } else { emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de la mise à jour du membre. Veuillez réessayer.', error: e, )); } } /// Supprime un membre Future _onDeleteMembre( DeleteMembre event, Emitter emit, ) async { try { emit(const MembresLoading()); await _deleteMember(event.id); emit(MembreDeleted(event.id)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de la suppression du membre. Veuillez réessayer.', error: e, )); } } /// Active un membre Future _onActivateMembre( ActivateMembre event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _repository.activateMembre(event.id); emit(MembreActivated(membre)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de l\'activation du membre. Veuillez réessayer.', error: e, )); } } /// Désactive un membre Future _onDeactivateMembre( DeactivateMembre event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _repository.deactivateMembre(event.id); emit(MembreDeactivated(membre)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de la désactivation du membre. Veuillez réessayer.', error: e, )); } } /// Réinitialise le mot de passe d'un membre Future _onResetMotDePasse( ResetMotDePasse event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _repository.resetMotDePasse(event.id); emit(MotDePasseReinitialise(membre)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de la réinitialisation du mot de passe. Veuillez réessayer.', error: e, )); } } /// Affecte un membre à une organisation (superadmin) Future _onAffecterOrganisation( AffecterOrganisation event, Emitter emit, ) async { try { emit(const MembresLoading()); final membre = await _repository.affecterOrganisation(event.membreId, event.organisationId); emit(MembreAffecte(membre)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de l\'affectation à l\'organisation. Veuillez réessayer.', error: e, )); } } /// Recherche avancée de membres Future _onSearchMembres( SearchMembres event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _searchMembers( criteria: event.criteria, page: event.page, size: event.size, ); emit(MembresLoaded( membres: result.membres, totalElements: result.totalElements, currentPage: result.currentPage, pageSize: result.pageSize, totalPages: result.totalPages, )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors de la recherche de membres. Veuillez réessayer.', error: e, )); } } /// Charge les membres actifs Future _onLoadActiveMembres( LoadActiveMembres event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _repository.getActiveMembers( page: event.page, size: event.size, ); emit(MembresLoaded( membres: result.membres, totalElements: result.totalElements, currentPage: result.currentPage, pageSize: result.pageSize, totalPages: result.totalPages, )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors du chargement des membres actifs. Veuillez réessayer.', error: e, )); } } /// Charge les membres du bureau Future _onLoadBureauMembres( LoadBureauMembres event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _repository.getBureauMembers( page: event.page, size: event.size, ); emit(MembresLoaded( membres: result.membres, totalElements: result.totalElements, currentPage: result.currentPage, pageSize: result.pageSize, totalPages: result.totalPages, )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors du chargement des membres du bureau. Veuillez réessayer.', error: e, )); } } /// Charge les statistiques Future _onLoadMembresStats( LoadMembresStats event, Emitter emit, ) async { try { emit(const MembresLoading()); final stats = await _getMemberStats(); emit(MembresStatsLoaded(stats)); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { if (e is DioException && e.type == DioExceptionType.cancel) return; emit(MembresError( message: 'Erreur lors du chargement des statistiques. Veuillez réessayer.', error: e, )); } } /// Extrait les erreurs de validation de la réponse Map _extractValidationErrors(dynamic data) { final errors = {}; if (data is Map && data.containsKey('errors')) { final errorsData = data['errors']; if (errorsData is Map) { errorsData.forEach((key, value) { errors[key] = value.toString(); }); } } return errors; } /// Génère un message d'erreur réseau approprié String _getNetworkErrorMessage(DioException e) { switch (e.type) { case DioExceptionType.connectionTimeout: return 'Délai de connexion dépassé. Vérifiez votre connexion internet.'; case DioExceptionType.sendTimeout: return 'Délai d\'envoi dépassé. Vérifiez votre connexion internet.'; case DioExceptionType.receiveTimeout: return 'Délai de réception dépassé. Vérifiez votre connexion internet.'; case DioExceptionType.badResponse: final statusCode = e.response?.statusCode; if (statusCode == 401) { return 'Non autorisé. Veuillez vous reconnecter.'; } else if (statusCode == 403) { return 'Accès refusé. Vous n\'avez pas les permissions nécessaires.'; } else if (statusCode == 404) { return 'Ressource non trouvée.'; } else if (statusCode == 409) { return 'Conflit. Cette ressource existe déjà.'; } else if (statusCode != null && statusCode >= 500) { return 'Erreur serveur. Veuillez réessayer plus tard.'; } return 'Erreur lors de la communication avec le serveur.'; case DioExceptionType.cancel: return 'Requête annulée.'; case DioExceptionType.unknown: return 'Erreur de connexion. Vérifiez votre connexion internet.'; default: return 'Erreur réseau inattendue.'; } } // ── Handlers cycle de vie des adhésions ────────────────────────────────── Future _onInviterMembre( InviterMembre event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _repository.inviterMembre( event.membreId, event.organisationId, roleOrg: event.roleOrg, ); emit(MembreInvite( membreId: event.membreId, organisationId: event.organisationId, nouveauStatut: result['statut']?.toString() ?? 'INVITE', )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { emit(MembresError(message: 'Erreur lors de l\'invitation du membre.', error: e)); } } Future _onActiverAdhesion( ActiverAdhesion event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _repository.activerAdhesion( event.membreId, event.organisationId, motif: event.motif, ); emit(AdhesionActivee( membreId: event.membreId, nouveauStatut: result['statut']?.toString() ?? 'ACTIF', )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { emit(MembresError(message: 'Erreur lors de l\'activation de l\'adhésion.', error: e)); } } Future _onSuspendrAdhesion( SuspendrAdhesion event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _repository.suspendrAdhesion( event.membreId, event.organisationId, motif: event.motif, ); emit(AdhesionSuspendue( membreId: event.membreId, nouveauStatut: result['statut']?.toString() ?? 'SUSPENDU', )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { emit(MembresError(message: 'Erreur lors de la suspension de l\'adhésion.', error: e)); } } Future _onRadierAdhesion( RadierAdhesion event, Emitter emit, ) async { try { emit(const MembresLoading()); final result = await _repository.radierAdhesion( event.membreId, event.organisationId, motif: event.motif, ); emit(MembreRadie( membreId: event.membreId, nouveauStatut: result['statut']?.toString() ?? 'RADIE', )); } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return; emit(MembresNetworkError( message: _getNetworkErrorMessage(e), code: e.response?.statusCode.toString(), error: e, )); } catch (e) { emit(MembresError(message: 'Erreur lors de la radiation du membre.', error: e)); } } }