feat(members): swipe par rôle + suppression SuperAdmin avec cascade UX

Swipe actions différenciées par rôle :
- SuperAdmin : → Reset MDP, ← Affecter Org
- OrgAdmin  : → Reset MDP, ← lifecycle selon statut (Suspendre/Activer/Réactiver)
  (masqué si cible = ORGADMIN/SUPERADMIN — cohérent avec guard backend)
- Autres rôles : → Reset MDP seulement

Suppression compte (SuperAdmin uniquement) :
- Nouveau callback onDeleteAccount dans MembersPage + MemberDetailPage
- Bouton rouge 'Supprimer ce compte' dans action sheet (zone destructive)
- Dialog de confirmation adaptatif dark/light avec badge admin si cible ORGADMIN
- Bouton caché si compte déjà désactivé (actif=false)
- Bannière 'Compte désactivé' visible sur page détail d'un compte soft-deleted
- BlocListener MembreDeleted : SnackBar + maybePop() + reload liste
- Bloc gère 409 Conflict (mono-admin) → MembresActionForbidden avec message backend

Nouvelles signatures :
- onLifecycleAction : (memberId, organisationId, action, motif) — inclut orgId
  pour permettre au SuperAdmin d'agir via l'org du membre lui-même
- 'actif' et 'roleCode' exposés dans la map via _convertMembreToMap
This commit is contained in:
dahoud
2026-04-15 20:14:08 +00:00
parent 55f84da49a
commit 36a903c80e
4 changed files with 1661 additions and 723 deletions

View File

@@ -278,6 +278,14 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
emit(MembreDeleted(event.id));
} on DioException catch (e) {
if (e.type == DioExceptionType.cancel) return;
// 409 Conflict = mono-admin orphelinage détecté par le backend.
// Émettre MembresActionForbidden avec le message métier du backend.
if (e.response?.statusCode == 409 || e.response?.statusCode == 403) {
final msg = _extractErrorMessage(e)
?? 'Suppression impossible : vérifiez que le membre n\'est pas le seul admin d\'une organisation.';
emit(MembresActionForbidden(message: msg, error: e));
return;
}
emit(MembresNetworkError(
message: _getNetworkErrorMessage(e),
code: e.response?.statusCode.toString(),
@@ -582,6 +590,18 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
}
}
/// Extrait le message d'erreur du corps JSON d'une réponse HTTP (champ "error"
/// ou "message"). Retourne null si non disponible.
String? _extractErrorMessage(DioException e) {
try {
final data = e.response?.data;
if (data is Map) {
return (data['message'] ?? data['error'])?.toString();
}
} catch (_) {}
return null;
}
// ── Handlers cycle de vie des adhésions ──────────────────────────────────
Future<void> _onInviterMembre(
@@ -629,6 +649,11 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
));
} on DioException catch (e) {
if (e.type == DioExceptionType.cancel) return;
if (e.response?.statusCode == 403) {
final msg = _extractErrorMessage(e) ?? 'Action non autorisée sur ce membre.';
emit(MembresActionForbidden(message: msg, error: e));
return;
}
emit(MembresNetworkError(
message: _getNetworkErrorMessage(e),
code: e.response?.statusCode.toString(),
@@ -656,6 +681,11 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
));
} on DioException catch (e) {
if (e.type == DioExceptionType.cancel) return;
if (e.response?.statusCode == 403) {
final msg = _extractErrorMessage(e) ?? 'Action non autorisée sur ce membre.';
emit(MembresActionForbidden(message: msg, error: e));
return;
}
emit(MembresNetworkError(
message: _getNetworkErrorMessage(e),
code: e.response?.statusCode.toString(),
@@ -683,6 +713,11 @@ class MembresBloc extends Bloc<MembresEvent, MembresState> {
));
} on DioException catch (e) {
if (e.type == DioExceptionType.cancel) return;
if (e.response?.statusCode == 403) {
final msg = _extractErrorMessage(e) ?? 'Action non autorisée sur ce membre.';
emit(MembresActionForbidden(message: msg, error: e));
return;
}
emit(MembresNetworkError(
message: _getNetworkErrorMessage(e),
code: e.response?.statusCode.toString(),