Files
unionflow-mobile-apps/lib/features/members/bloc/membres_bloc.dart
dahoud 70cbd1c873 fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod
Auth:
- profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password

Multi-org (Phase 3):
- OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry
- org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role

Navigation:
- MorePage: navigation conditionnelle par typeOrganisation
- Suppression adaptive_navigation (remplacé par main_navigation_layout)

Auth AppAuth:
- keycloak_webview_auth_service: fixes AppAuth Android
- AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet

Onboarding:
- Nouveaux états: payment_method_page, onboarding_shared_widgets
- SouscriptionStatusModel mis à jour StatutValidationSouscription

Android:
- build.gradle: ProGuard/R8, network_security_config
- Gradle wrapper mis à jour
2026-04-07 20:56:03 +00:00

697 lines
21 KiB
Dart

/// 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<MembresEvent, MembresState> {
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<WebSocketEvent>? _webSocketSubscription;
MembresBloc(
this._getMembers,
this._getMemberById,
this._createMember,
this._updateMember,
this._deleteMember,
this._searchMembers,
this._getMemberStats,
this._repository,
this._webSocketService,
) : super(const MembresInitial()) {
on<LoadMembres>(_onLoadMembres);
on<LoadMembreById>(_onLoadMembreById);
on<CreateMembre>(_onCreateMembre);
on<UpdateMembre>(_onUpdateMembre);
on<DeleteMembre>(_onDeleteMembre);
on<ActivateMembre>(_onActivateMembre);
on<DeactivateMembre>(_onDeactivateMembre);
on<SearchMembres>(_onSearchMembres);
on<LoadActiveMembres>(_onLoadActiveMembres);
on<LoadBureauMembres>(_onLoadBureauMembres);
on<LoadMembresStats>(_onLoadMembresStats);
on<ResetMotDePasse>(_onResetMotDePasse);
on<AffecterOrganisation>(_onAffecterOrganisation);
on<InviterMembre>(_onInviterMembre);
on<ActiverAdhesion>(_onActiverAdhesion);
on<SuspendrAdhesion>(_onSuspendrAdhesion);
on<RadierAdhesion>(_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<void> close() {
_webSocketSubscription?.cancel();
return super.close();
}
/// Charge la liste des membres
Future<void> _onLoadMembres(
LoadMembres event,
Emitter<MembresState> 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<void> _onLoadMembreById(
LoadMembreById event,
Emitter<MembresState> 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<void> _onCreateMembre(
CreateMembre event,
Emitter<MembresState> 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<void> _onUpdateMembre(
UpdateMembre event,
Emitter<MembresState> 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<void> _onDeleteMembre(
DeleteMembre event,
Emitter<MembresState> 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<void> _onActivateMembre(
ActivateMembre event,
Emitter<MembresState> 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<void> _onDeactivateMembre(
DeactivateMembre event,
Emitter<MembresState> 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<void> _onResetMotDePasse(
ResetMotDePasse event,
Emitter<MembresState> 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<void> _onAffecterOrganisation(
AffecterOrganisation event,
Emitter<MembresState> 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<void> _onSearchMembres(
SearchMembres event,
Emitter<MembresState> 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<void> _onLoadActiveMembres(
LoadActiveMembres event,
Emitter<MembresState> 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<void> _onLoadBureauMembres(
LoadBureauMembres event,
Emitter<MembresState> 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<void> _onLoadMembresStats(
LoadMembresStats event,
Emitter<MembresState> 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<String, String> _extractValidationErrors(dynamic data) {
final errors = <String, String>{};
if (data is Map<String, dynamic> && data.containsKey('errors')) {
final errorsData = data['errors'];
if (errorsData is Map<String, dynamic>) {
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<void> _onInviterMembre(
InviterMembre event,
Emitter<MembresState> 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<void> _onActiverAdhesion(
ActiverAdhesion event,
Emitter<MembresState> 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<void> _onSuspendrAdhesion(
SuspendrAdhesion event,
Emitter<MembresState> 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<void> _onRadierAdhesion(
RadierAdhesion event,
Emitter<MembresState> 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));
}
}
}