/// Wrapper BLoC pour la page des membres /// /// Ce fichier enveloppe la MembersPage existante avec le MembresBloc /// pour connecter l'UI riche existante à l'API backend réelle. library members_page_wrapper; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import '../../../../shared/design_system/tokens/color_tokens.dart'; import '../../../../shared/widgets/error_widget.dart'; import '../../../../shared/widgets/loading_widget.dart'; import '../../../../core/utils/logger.dart'; import '../../bloc/membres_bloc.dart'; import '../../bloc/membres_event.dart'; import '../../bloc/membres_state.dart'; import '../../data/models/membre_complete_model.dart'; import '../widgets/add_member_dialog.dart' show showAddMemberSheet, showCredentialsDialog; import 'members_page_connected.dart'; final _getIt = GetIt.instance; /// Wrapper qui fournit le BLoC à la page des membres class MembersPageWrapper extends StatelessWidget { final String? organisationId; const MembersPageWrapper({super.key, this.organisationId}); @override Widget build(BuildContext context) { AppLogger.info('MembersPageWrapper: Création du BlocProvider'); return BlocProvider( create: (context) { AppLogger.info('MembresPageWrapper: Initialisation du MembresBloc'); final bloc = _getIt(); // Charger les membres au démarrage bloc.add(LoadMembres(organisationId: organisationId)); return bloc; }, child: MembersPageConnected(organisationId: organisationId), ); } } /// Page des membres connectée au BLoC /// /// Cette page gère les états du BLoC et affiche l'UI appropriée class MembersPageConnected extends StatelessWidget { final String? organisationId; const MembersPageConnected({super.key, this.organisationId}); @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { // Après création : recharger la liste (la dialog mot de passe est gérée dans AddMemberDialog) if (state is MembreCreated) { context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } // Accès refusé sur une action lifecycle (403) — SnackBar ciblé + rechargement liste if (state is MembresActionForbidden) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row(children: [ const Icon(Icons.block_outlined, color: Colors.white, size: 18), const SizedBox(width: 8), Expanded(child: Text(state.message)), ]), backgroundColor: ColorTokens.error, duration: const Duration(seconds: 5), ), ); // Recharger pour revenir à l'état MembresLoaded (évite la page d'erreur plein écran) context.read().add(LoadMembres(organisationId: organisationId)); return; } // Gestion des erreurs avec SnackBar if (state is MembresError) { final bloc = context.read(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: ColorTokens.error, duration: const Duration(seconds: 4), action: SnackBarAction( label: 'Réessayer', textColor: Colors.white, onPressed: () { bloc.add(LoadMembres(organisationId: organisationId)); }, ), ), ); } // Après activation : succès + rechargement if (state is MembreActivated) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Membre activé avec succès'), backgroundColor: ColorTokens.success, duration: Duration(seconds: 3), ), ); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } // Après suppression définitive : feedback + pop la page de détail si ouverte + rechargement if (state is MembreDeleted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row(children: const [ Icon(Icons.delete_forever, color: Colors.white, size: 18), SizedBox(width: 8), Expanded(child: Text('Compte supprimé avec succès')), ]), backgroundColor: ColorTokens.error, duration: const Duration(seconds: 3), ), ); // Si on est dans MemberDetailPage, revenir à la liste (pop unique) Navigator.of(context).maybePop(); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } // Après reset mot de passe : afficher le dialog credentials if (state is MotDePasseReinitialise) { showCredentialsDialog(context, state.membre); } // Après affectation à une organisation : succès + rechargement if (state is MembreAffecte) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Membre affecté à l\'organisation avec succès'), backgroundColor: ColorTokens.success, duration: Duration(seconds: 3), ), ); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } // Lifecycle adhésion : succès + rechargement if (state is MembreInvite) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Invitation envoyée'), backgroundColor: ColorTokens.info, duration: Duration(seconds: 3)), ); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } if (state is AdhesionActivee) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Adhésion activée'), backgroundColor: ColorTokens.success, duration: Duration(seconds: 3)), ); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } if (state is AdhesionSuspendue) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Adhésion suspendue'), backgroundColor: ColorTokens.warning, duration: Duration(seconds: 3)), ); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } if (state is MembreRadie) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Membre radié de l\'organisation'), backgroundColor: ColorTokens.error, duration: Duration(seconds: 3)), ); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); } }, child: BlocBuilder( builder: (context, state) { AppLogger.blocState('MembresBloc', state.runtimeType.toString()); // État initial if (state is MembresInitial) { return Container( color: null, child: const Center( child: AppLoadingWidget(message: 'Initialisation...'), ), ); } // État de chargement if (state is MembresLoading) { return Container( color: null, child: const Center( child: AppLoadingWidget(message: 'Chargement des membres...'), ), ); } // État de rafraîchissement (afficher l'UI avec un indicateur) if (state is MembresRefreshing) { // Affiche un indicateur pendant le rafraîchissement return Container( color: null, child: const Center( child: AppLoadingWidget(message: 'Actualisation...'), ), ); } // Après création : on recharge la liste (listener a dispatché LoadMembres) if (state is MembreCreated) { return Container( color: null, child: const Center( child: AppLoadingWidget(message: 'Actualisation...'), ), ); } // État chargé avec succès if (state is MembresLoaded) { final membres = state.membres; AppLogger.info('MembresPageConnected: ${membres.length} membres chargés'); // Convertir les membres en format Map pour l'UI existante final membersData = _convertMembersToMapList(membres); return MembersPageWithDataAndPagination( members: membersData, totalCount: state.totalElements, currentPage: state.currentPage, totalPages: state.totalPages, organisationId: organisationId, onPageChanged: (newPage, recherche) { AppLogger.userAction('Load page', data: {'page': newPage}); context.read().add(LoadMembres(page: newPage, recherche: recherche, organisationId: organisationId)); }, onRefresh: () { AppLogger.userAction('Refresh membres'); context.read().add(LoadMembres(refresh: true, organisationId: organisationId)); }, onSearch: (query) { context.read().add(LoadMembres(page: 0, recherche: query, organisationId: organisationId)); }, onAddMember: () => showAddMemberSheet(context), onActivateMember: (memberId) { context.read().add(ActivateMembre(memberId)); }, onResetPassword: (memberId) { context.read().add(ResetMotDePasse(memberId)); }, onAffecterOrganisation: organisationId == null ? (memberId, orgId) { context.read().add(AffecterOrganisation(memberId, orgId)); } : null, // Suppression définitive — SuperAdmin uniquement (organisationId == null) // Backend : DELETE /api/membres/{id} @RolesAllowed({ ADMIN, SUPER_ADMIN }) onDeleteAccount: organisationId == null ? (memberId) { context.read().add(DeleteMembre(memberId)); } : null, // onLifecycleAction est toujours fourni. // orgAdmin : organisationId fixe (contexte org). // superAdmin : organisationId provient du membre lui-même (passé par _lifecycleButtons). onLifecycleAction: (memberId, targetOrgId, action, motif) { final bloc = context.read(); // En contexte orgAdmin on utilise l'org fixe, en superAdmin l'org du membre final effectiveOrgId = organisationId ?? targetOrgId; switch (action) { case 'inviter': bloc.add(InviterMembre(membreId: memberId, organisationId: effectiveOrgId)); break; case 'activer': bloc.add(ActiverAdhesion(membreId: memberId, organisationId: effectiveOrgId, motif: motif)); break; case 'suspendre': bloc.add(SuspendrAdhesion(membreId: memberId, organisationId: effectiveOrgId, motif: motif)); break; case 'radier': bloc.add(RadierAdhesion(membreId: memberId, organisationId: effectiveOrgId, motif: motif)); break; } }, ); } // État d'erreur réseau if (state is MembresNetworkError) { AppLogger.error('MembersPageConnected: Erreur réseau', error: state.message); return Container( color: null, child: NetworkErrorWidget( onRetry: () { AppLogger.userAction('Retry load membres after network error'); context.read().add(LoadMembres(organisationId: organisationId)); }, ), ); } // État d'erreur générale if (state is MembresError) { AppLogger.error('MembersPageConnected: Erreur', error: state.message); return Container( color: null, child: AppErrorWidget( message: state.message, onRetry: () { AppLogger.userAction('Retry load membres after error'); context.read().add(LoadMembres(organisationId: organisationId)); }, ), ); } // État par défaut (ne devrait jamais arriver) AppLogger.warning('MembersPageConnected: État non géré: ${state.runtimeType}'); return Container( color: null, child: const Center( child: AppLoadingWidget(message: 'Chargement...'), ), ); }, ), ); } /// Convertit une liste de MembreCompletModel en List> /// pour compatibilité avec l'UI existante List> _convertMembersToMapList(List membres) { return membres.map((membre) => _convertMembreToMap(membre)).toList(); } /// Convertit un MembreCompletModel en Map Map _convertMembreToMap(MembreCompletModel membre) { return { 'id': membre.id ?? '', 'name': membre.nomComplet, 'email': membre.email, 'actif': membre.actif, // flag global désactivation (soft delete) 'roleCode': membre.role, // code brut ex: ORGADMIN, SUPERADMIN — utilisé pour les checks de permission 'role': _mapRoleToString(membre.role), 'status': _mapStatutToString(membre.statut), 'joinDate': membre.dateAdhesion, 'lastActivity': membre.derniereActivite ?? DateTime.now(), 'avatar': membre.photo, 'phone': membre.telephone ?? '', 'department': membre.profession ?? '', 'location': '${membre.ville ?? ''}, ${membre.pays ?? ''}', 'permissions': 15, // Valeurs par défaut tant que l'API ne fournit pas permissions 'contributionScore': 0, // Valeurs par défaut tant que l'API ne fournit pas contributionScore 'eventsAttended': membre.nombreEvenementsParticipes, 'projectsInvolved': 0, // Valeurs par défaut tant que l'API ne fournit pas projectsInvolved // Champs supplémentaires du modèle 'prenom': membre.prenom, 'nom': membre.nom, 'dateNaissance': membre.dateNaissance, 'genre': membre.genre?.name, 'adresse': membre.adresse, 'ville': membre.ville, 'codePostal': membre.codePostal, 'region': membre.region, 'pays': membre.pays, 'profession': membre.profession, 'nationalite': membre.nationalite, 'organisationId': membre.organisationId, 'membreBureau': membre.membreBureau, 'responsable': membre.responsable, 'fonctionBureau': membre.fonctionBureau, 'numeroMembre': membre.numeroMembre, 'cotisationAJour': membre.cotisationAJour, 'organisationNom': membre.organisationNom, 'statutKyc': membre.statutKyc?.name, 'niveauVigilanceKyc': membre.niveauVigilanceKyc?.name, 'statutMembre': membre.statut?.name, // Propriétés calculées 'initiales': membre.initiales, 'age': membre.age, 'estActifEtAJour': membre.estActifEtAJour, }; } /// Mappe le rôle du modèle vers une chaîne lisible String _mapRoleToString(String? role) { if (role == null) return 'Membre Simple'; switch (role.toLowerCase()) { case 'superadmin': return 'Super Administrateur'; case 'orgadmin': return 'Administrateur Org'; case 'moderator': return 'Modérateur'; case 'activemember': return 'Membre Actif'; case 'simplemember': return 'Membre Simple'; case 'visitor': return 'Visiteur'; default: return role; } } /// Mappe le statut du modèle vers une chaîne lisible String _mapStatutToString(StatutMembre? statut) { if (statut == null) return 'Actif'; switch (statut) { case StatutMembre.actif: return 'Actif'; case StatutMembre.inactif: return 'Inactif'; case StatutMembre.suspendu: return 'Suspendu'; case StatutMembre.enAttente: return 'En attente'; } } }