import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/design_system/components/uf_app_bar.dart'; import '../../../../core/constants/app_constants.dart'; import '../../../../features/organizations/domain/repositories/organization_repository.dart'; /// Annuaire des Membres — Design UnionFlow v2 /// /// Fonctionnalités : /// - Identité violet #7616E8 /// - Header KPI 4 métriques (Total, % Actifs, En attente, Cotisations) /// - Recherche inline collapsible + filtres chips /// - Scroll infini (append pages au fur et à mesure) /// - Mode grille / liste toggle /// - Swipe gauche → Reset MDP | Swipe droit → Affecter org /// - Long press → sélection multiple (Exporter, Affecter org) /// - Navigation page complète au lieu d'une bottom sheet class MembersPageWithDataAndPagination extends StatefulWidget { final List> members; final int totalCount; final int currentPage; final int totalPages; final Function(int page, String? recherche) onPageChanged; final VoidCallback onRefresh; final void Function(String? query)? onSearch; final VoidCallback? onAddMember; /// null = SUPER_ADMIN (vue globale, affiche l'organisation sur chaque carte) final String? organisationId; final void Function(String memberId)? onActivateMember; final void Function(String memberId)? onResetPassword; final void Function(String memberId, String organisationId)? onAffecterOrganisation; final void Function(String memberId, String organisationId, String action, String? motif)? onLifecycleAction; /// Suppression définitive du compte (SuperAdmin uniquement — backend @RolesAllowed ADMIN/SUPER_ADMIN). final void Function(String memberId)? onDeleteAccount; const MembersPageWithDataAndPagination({ super.key, required this.members, required this.totalCount, required this.currentPage, required this.totalPages, required this.onPageChanged, required this.onRefresh, this.onSearch, this.onAddMember, this.organisationId, this.onActivateMember, this.onResetPassword, this.onAffecterOrganisation, this.onLifecycleAction, this.onDeleteAccount, }); @override State createState() => _MembersPageState(); } class _MembersPageState extends State { final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); String _searchQuery = ''; String _filterStatus = 'Tous'; Timer? _searchDebounce; bool _isSearchExpanded = false; bool _isGridView = false; bool _isSelectionMode = false; final Set _selectedIds = {}; bool _isLoadingMore = false; List> _accumulatedMembers = []; List> _organisationsPicker = []; bool get _isSuperAdmin => widget.organisationId == null; @override void initState() { super.initState(); _accumulatedMembers = List.from(widget.members); _scrollController.addListener(_onScroll); } @override void didUpdateWidget(covariant MembersPageWithDataAndPagination old) { super.didUpdateWidget(old); if (old.members != widget.members) { setState(() { if (widget.currentPage == 0) { _accumulatedMembers = List.from(widget.members); } else { // Append page suivante (dédoublonnage par id) final existingIds = _accumulatedMembers.map((m) => m['id'] as String? ?? '').toSet(); final newItems = widget.members.where((m) => !existingIds.contains(m['id'] as String? ?? '')).toList(); _accumulatedMembers.addAll(newItems); } _isLoadingMore = false; }); } } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { final pos = _scrollController.position; if (pos.pixels >= pos.maxScrollExtent - 200) { if (!_isLoadingMore && widget.currentPage < widget.totalPages - 1) { setState(() => _isLoadingMore = true); widget.onPageChanged(widget.currentPage + 1, _searchQuery.isEmpty ? null : _searchQuery); } } } void _clearSearch() { _searchDebounce?.cancel(); _searchController.clear(); setState(() => _searchQuery = ''); widget.onSearch?.call(null); } void _toggleSelection(String id) { setState(() { if (_selectedIds.contains(id)) { _selectedIds.remove(id); if (_selectedIds.isEmpty) _isSelectionMode = false; } else { _selectedIds.add(id); } }); } void _exitSelectionMode() { setState(() { _isSelectionMode = false; _selectedIds.clear(); }); } List> get _filteredMembers { return _accumulatedMembers.where((m) { final matchesSearch = _searchQuery.isEmpty || (m['name'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) || (m['email'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) || (m['numeroMembre'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()); final matchesStatus = _filterStatus == 'Tous' || m['status'] == _filterStatus; return matchesSearch && matchesStatus; }).toList(); } // ═══════════════════════════════════════════════════════════════════════════ // Build // ═══════════════════════════════════════════════════════════════════════════ @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: _isSelectionMode ? _buildSelectionAppBar() : _buildMainAppBar(), body: Column( children: [ _buildKpiHeader(context), AnimatedCrossFade( duration: const Duration(milliseconds: 220), firstCurve: Curves.easeInOut, secondCurve: Curves.easeInOut, sizeCurve: Curves.easeInOut, crossFadeState: _isSearchExpanded ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: _buildSearchPanel(context), secondChild: const SizedBox(width: double.infinity, height: 0), ), Expanded(child: _buildContent(context)), ], ), ); } // ─── AppBar principale ──────────────────────────────────────────────────── PreferredSizeWidget _buildMainAppBar() { final hasFilter = _filterStatus != 'Tous' || _searchQuery.isNotEmpty; return UFAppBar( title: 'Annuaire Membres', moduleGradient: ModuleColors.membresGradient, actions: [ Stack( children: [ IconButton( icon: Icon(_isSearchExpanded ? Icons.search_off : Icons.search), onPressed: () => setState(() => _isSearchExpanded = !_isSearchExpanded), tooltip: 'Recherche & filtres', ), if (hasFilter) Positioned( right: 10, top: 10, child: Container( width: 7, height: 7, decoration: BoxDecoration(color: AppColors.warning, shape: BoxShape.circle), ), ), ], ), IconButton( icon: Icon(_isGridView ? Icons.view_list_outlined : Icons.grid_view_outlined), onPressed: () => setState(() => _isGridView = !_isGridView), tooltip: _isGridView ? 'Vue liste' : 'Vue grille', ), if (widget.onAddMember != null) IconButton( icon: const Icon(Icons.person_add_outlined), onPressed: widget.onAddMember, tooltip: 'Ajouter un membre', ), const SizedBox(width: 4), ], ); } // ─── AppBar sélection multiple ──────────────────────────────────────────── PreferredSizeWidget _buildSelectionAppBar() { return AppBar( backgroundColor: ModuleColors.membres, foregroundColor: AppColors.onPrimary, elevation: 0, iconTheme: const IconThemeData(color: AppColors.onPrimary), actionsIconTheme: const IconThemeData(color: AppColors.onPrimary), leading: IconButton( icon: const Icon(Icons.close), onPressed: _exitSelectionMode, ), title: Text( '${_selectedIds.length} sélectionné(s)', style: const TextStyle(color: AppColors.onPrimary, fontWeight: FontWeight.w600), ), actions: [ if (_selectedIds.isNotEmpty && widget.onAffecterOrganisation != null && _isSuperAdmin) IconButton( icon: const Icon(Icons.business_outlined), tooltip: 'Affecter à une organisation', onPressed: _showBulkAffecterDialog, ), if (_selectedIds.isNotEmpty) IconButton( icon: const Icon(Icons.download_outlined), tooltip: 'Exporter la sélection', onPressed: _bulkExporter, ), const SizedBox(width: 4), ], ); } // ─── Header KPI ──────────────────────────────────────────────────────────── Widget _buildKpiHeader(BuildContext context) { final total = widget.totalCount; final displayed = _accumulatedMembers.length; final activeCount = _accumulatedMembers.where((m) => m['status'] == 'Actif').length; final pendingCount = _accumulatedMembers.where((m) => m['status'] == 'En attente').length; final cotisOkCount = _accumulatedMembers.where((m) => m['cotisationAJour'] == true).length; final activePercent = displayed > 0 ? activeCount / displayed : 0.0; final cotisPercent = displayed > 0 ? cotisOkCount / displayed : 0.0; return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border(bottom: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 1)), ), child: Row( children: [ _kpiTile(context, 'Total', total.toString(), ModuleColors.membres, Icons.people_outline), _kpiSeparator(context), _kpiTileProgress(context, 'Actifs', '${(activePercent * 100).toStringAsFixed(0)}%', AppColors.success, activePercent), _kpiSeparator(context), _kpiTile(context, 'Attente', pendingCount.toString(), AppColors.warning, Icons.schedule_outlined), _kpiSeparator(context), _kpiTileProgress( context, 'Cotisations', '${(cotisPercent * 100).toStringAsFixed(0)}%', cotisPercent >= 0.7 ? AppColors.success : AppColors.error, cotisPercent, ), ], ), ); } Widget _kpiTile(BuildContext context, String label, String value, Color color, IconData icon) { return Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: color), const SizedBox(height: 1), Text(value, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: color)), Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color.withOpacity(0.75)), textAlign: TextAlign.center), ], ), ); } Widget _kpiTileProgress(BuildContext context, String label, String value, Color color, double progress) { return Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(value, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: color)), const SizedBox(height: 3), ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: progress, backgroundColor: color.withOpacity(0.15), valueColor: AlwaysStoppedAnimation(color), minHeight: 4, ), ), const SizedBox(height: 2), Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color.withOpacity(0.75)), textAlign: TextAlign.center), ], ), ); } Widget _kpiSeparator(BuildContext context) => Container(width: 1, height: 42, color: Theme.of(context).colorScheme.outlineVariant); // ─── Panneau recherche collapsible ──────────────────────────────────────── Widget _buildSearchPanel(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border(bottom: BorderSide(color: Theme.of(context).colorScheme.outlineVariant, width: 1)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _searchController, onChanged: (v) { setState(() => _searchQuery = v); _searchDebounce?.cancel(); _searchDebounce = Timer(AppConstants.searchDebounce, () { widget.onSearch?.call(v.isEmpty ? null : v); }); }, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( hintText: 'Nom, email, numéro membre...', hintStyle: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant), prefixIcon: Icon(Icons.search, size: 20, color: ModuleColors.membres), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 18), onPressed: _clearSearch, ) : null, contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).colorScheme.outline)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).colorScheme.outline)), focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: ModuleColors.membres, width: 1.5)), filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerHighest, ), ), const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: ['Tous', 'Actif', 'Inactif', 'En attente', 'Suspendu'] .map((f) => Padding( padding: const EdgeInsets.only(right: 8), child: _buildFilterChip(context, f), )) .toList(), ), ), ], ), ); } Widget _buildFilterChip(BuildContext context, String label) { final isSelected = _filterStatus == label; return GestureDetector( onTap: () => setState(() => _filterStatus = label), child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isSelected ? ModuleColors.membres : Colors.transparent, borderRadius: BorderRadius.circular(20), border: Border.all(color: isSelected ? ModuleColors.membres : Theme.of(context).colorScheme.outline, width: 1), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: isSelected ? AppColors.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ); } // ─── Contenu principal ──────────────────────────────────────────────────── Widget _buildContent(BuildContext context) { final filtered = _filteredMembers; return RefreshIndicator( onRefresh: () async { setState(() => _accumulatedMembers.clear()); widget.onRefresh(); }, color: ModuleColors.membres, child: filtered.isEmpty ? SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: _buildEmptyState(context), ) : _isGridView ? _buildGridView(context, filtered) : _buildListView(context, filtered), ); } Widget _buildListView(BuildContext context, List> members) { return ListView.separated( controller: _scrollController, padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), itemCount: members.length + (_isLoadingMore ? 1 : 0), separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, i) { if (i == members.length) { return const Center( child: Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(color: ModuleColors.membres), ), ); } return _buildSwipeableCard(context, members[i]); }, ); } Widget _buildGridView(BuildContext context, List> members) { return GridView.builder( controller: _scrollController, padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.82, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: members.length + (_isLoadingMore ? 1 : 0), itemBuilder: (context, i) { if (i == members.length) { return const Center(child: CircularProgressIndicator(color: ModuleColors.membres)); } return _buildGridCard(context, members[i]); }, ); } // ─── Carte liste avec swipe ──────────────────────────────────────────────── /// Détermine l'action swipe gauche pour l'orgAdmin selon le statut du membre. /// Retourne null si aucune action n'est disponible (rôle admin, statut final, etc.). _SwipeAction? _getLeftSwipeAction(Map member) { // SuperAdmin : affecter org (géré séparément) if (_isSuperAdmin) return null; // Pas de lifecycle disponible if (widget.onLifecycleAction == null) return null; // Pas d'action sur un admin/superadmin final roleCode = (member['roleCode'] as String? ?? '').toUpperCase(); if (roleCode == 'ORGADMIN' || roleCode == 'SUPERADMIN' || roleCode == 'ADMIN_ORGANISATION' || roleCode == 'SUPER_ADMIN') { return null; } final statut = (member['statutMembre'] as String? ?? member['status'] as String? ?? '') .toUpperCase(); switch (statut) { case 'EN_ATTENTE_VALIDATION': case 'EN_ATTENTE': case 'INVITE': return const _SwipeAction( action: 'activer', icon: Icons.check_circle_outline, label: 'Activer', color: AppColors.success, ); case 'ACTIF': return const _SwipeAction( action: 'suspendre', icon: Icons.pause_circle_outline, label: 'Suspendre', color: AppColors.warning, ); case 'SUSPENDU': return const _SwipeAction( action: 'activer', icon: Icons.play_circle_outline, label: 'Réactiver', color: AppColors.info, ); default: // RADIE, ARCHIVE, etc. → pas d'action rapide return null; } } Widget _buildSwipeableCard(BuildContext context, Map member) { final id = member['id'] as String? ?? ''; if (_isSelectionMode) { return _buildListCard(context, member, isSelected: _selectedIds.contains(id)); } // Swipe droite → Reset MDP (tous rôles admin si callback fourni) final canSwipeRight = widget.onResetPassword != null; // Swipe gauche → SuperAdmin: affecter org | OrgAdmin: lifecycle selon statut final bool canSwipeLeftSuperAdmin = _isSuperAdmin && widget.onAffecterOrganisation != null; final _SwipeAction? leftAction = _getLeftSwipeAction(member); final bool canSwipeLeft = canSwipeLeftSuperAdmin || leftAction != null; if (!canSwipeRight && !canSwipeLeft) { return _buildListCard(context, member); } // Background droite (Reset MDP) — toujours warning/orange final rightBg = _swipeActionBackground( isLeft: true, icon: Icons.lock_reset_outlined, label: 'Reset\nMDP', color: AppColors.warning, ); // Background gauche — contextuel selon rôle Widget leftBg; if (canSwipeLeftSuperAdmin) { leftBg = _swipeActionBackground( isLeft: false, icon: Icons.business_outlined, label: 'Affecter\nOrg', color: ModuleColors.membres, ); } else if (leftAction != null) { leftBg = _swipeActionBackground( isLeft: false, icon: leftAction.icon, label: leftAction.label, color: leftAction.color, ); } else { leftBg = const SizedBox.shrink(); } return Dismissible( key: Key('swipe_$id'), direction: canSwipeRight && canSwipeLeft ? DismissDirection.horizontal : canSwipeRight ? DismissDirection.startToEnd : DismissDirection.endToStart, confirmDismiss: (dir) async { if (dir == DismissDirection.startToEnd && canSwipeRight) { widget.onResetPassword!(id); } else if (dir == DismissDirection.endToStart) { if (canSwipeLeftSuperAdmin) { _showAffecterOrganisationDialog(context, member); } else if (leftAction != null) { final orgId = member['organisationId'] as String? ?? ''; if (orgId.isNotEmpty) { widget.onLifecycleAction!(id, orgId, leftAction.action, null); } } } return false; // la carte ne disparaît pas }, background: canSwipeRight ? rightBg : const SizedBox.shrink(), secondaryBackground: canSwipeLeft ? leftBg : const SizedBox.shrink(), child: _buildListCard(context, member), ); } Widget _swipeActionBackground({ required bool isLeft, required IconData icon, required String label, required Color color, }) { return Container( margin: const EdgeInsets.symmetric(vertical: 1), decoration: BoxDecoration( color: color.withOpacity(0.13), borderRadius: BorderRadius.circular(12), ), alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight, padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color, size: 22), const SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: color), textAlign: TextAlign.center, ), ], ), ); } // ─── Carte liste ────────────────────────────────────────────────────────── Widget _buildListCard(BuildContext context, Map member, {bool isSelected = false}) { final id = member['id'] as String? ?? ''; final status = member['status'] as String? ?? '?'; final orgName = member['organisationNom'] as String?; final numero = member['numeroMembre'] as String?; final cotisAJour = member['cotisationAJour'] as bool? ?? false; final statutKyc = member['statutKyc'] as String?; return GestureDetector( onTap: () { if (_isSelectionMode) { _toggleSelection(id); } else { _navigateToDetail(context, member); } }, onLongPress: () { if (!_isSelectionMode) { setState(() { _isSelectionMode = true; _selectedIds.add(id); }); } }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isSelected ? ModuleColors.membres.withOpacity(0.08) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? ModuleColors.membres : Theme.of(context).colorScheme.outlineVariant, width: isSelected ? 1.5 : 1, ), ), child: Row( children: [ // Checkbox ou Avatar if (_isSelectionMode) Container( width: 36, height: 36, decoration: BoxDecoration( shape: BoxShape.circle, color: isSelected ? ModuleColors.membres : Colors.transparent, border: Border.all(color: isSelected ? ModuleColors.membres : Theme.of(context).colorScheme.outline, width: 2), ), child: isSelected ? const Icon(Icons.check, size: 18, color: AppColors.onPrimary) : null, ) else _buildAvatarWidget(member, 36), const SizedBox(width: 12), // Infos Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( member['name'] as String? ?? 'Inconnu', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface), overflow: TextOverflow.ellipsis, ), ), if (numero != null && numero.isNotEmpty) ...[ const SizedBox(width: 6), Text( numero, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ], ), const SizedBox(height: 2), Text( member['role'] as String? ?? 'Membre', style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), ), if (_isSuperAdmin && orgName != null && orgName.isNotEmpty) ...[ const SizedBox(height: 3), Row( children: [ const Icon(Icons.business_outlined, size: 11, color: ModuleColors.membres), const SizedBox(width: 4), Expanded( child: Text( orgName, style: const TextStyle(fontSize: 11, color: ModuleColors.membres, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, ), ), ], ), ], // Indicateurs KYC + Cotisation const SizedBox(height: 5), Row( children: [ _miniIndicator( cotisAJour ? Icons.payments_outlined : Icons.money_off_outlined, cotisAJour ? AppColors.success : AppColors.error, cotisAJour ? 'Cotis. OK' : 'Cotis. NOK', ), if (statutKyc != null) ...[ const SizedBox(width: 5), _kycMiniIndicator(statutKyc), ], ], ), ], ), ), const SizedBox(width: 8), _statusBadge(status), ], ), ), ); } Widget _miniIndicator(IconData icon, Color color, String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withOpacity(0.3), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 10, color: color), const SizedBox(width: 3), Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color)), ], ), ); } Widget _kycMiniIndicator(String statutKyc) { final (Color color, IconData icon) = switch (statutKyc) { 'VERIFIE' => (AppColors.success, Icons.verified_user_outlined), 'EN_COURS' => (AppColors.warning, Icons.pending_outlined), 'REJETE' => (AppColors.error, Icons.gpp_bad_outlined), _ => (AppColors.textTertiary, Icons.help_outline), }; return _miniIndicator(icon, color, 'KYC'); } // ─── Carte grille ───────────────────────────────────────────────────────── Widget _buildGridCard(BuildContext context, Map member) { final id = member['id'] as String? ?? ''; final status = member['status'] as String? ?? '?'; final cotisAJour = member['cotisationAJour'] as bool? ?? false; final statutKyc = member['statutKyc'] as String?; final isSelected = _selectedIds.contains(id); return GestureDetector( onTap: () => _isSelectionMode ? _toggleSelection(id) : _navigateToDetail(context, member), onLongPress: () { if (!_isSelectionMode) { setState(() { _isSelectionMode = true; _selectedIds.add(id); }); } }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), decoration: BoxDecoration( color: isSelected ? ModuleColors.membres.withOpacity(0.08) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(14), border: Border.all( color: isSelected ? ModuleColors.membres : Theme.of(context).colorScheme.outlineVariant, width: isSelected ? 1.5 : 1, ), ), child: Padding( padding: const EdgeInsets.all(12), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Stack( children: [ _buildAvatarWidget(member, 48), if (isSelected) Positioned( right: 0, bottom: 0, child: Container( width: 18, height: 18, decoration: const BoxDecoration(color: ModuleColors.membres, shape: BoxShape.circle), child: const Icon(Icons.check, size: 12, color: AppColors.onPrimary), ), ), ], ), const SizedBox(height: 8), Text( member['name'] as String? ?? 'Inconnu', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 5), _statusBadge(status), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(cotisAJour ? Icons.payments_outlined : Icons.money_off_outlined, size: 14, color: cotisAJour ? AppColors.success : AppColors.error), if (statutKyc != null) ...[ const SizedBox(width: 6), Icon( switch (statutKyc) { 'VERIFIE' => Icons.verified_user_outlined, 'EN_COURS' => Icons.pending_outlined, _ => Icons.gpp_bad_outlined, }, size: 14, color: switch (statutKyc) { 'VERIFIE' => AppColors.success, 'EN_COURS' => AppColors.warning, _ => AppColors.error, }, ), ], ], ), ], ), ), ), ); } // ─── Avatar violet ──────────────────────────────────────────────────────── Widget _buildAvatarWidget(Map member, double size) { return Container( width: size, height: size, decoration: const BoxDecoration( gradient: LinearGradient(colors: [ModuleColors.membresDark, ModuleColors.membres], begin: Alignment.topLeft, end: Alignment.bottomRight), shape: BoxShape.circle, ), alignment: Alignment.center, child: Text( member['initiales'] as String? ?? '??', style: TextStyle(color: AppColors.onPrimary, fontWeight: FontWeight.w700, fontSize: size * 0.36), ), ); } // ─── Badge statut ───────────────────────────────────────────────────────── Widget _statusBadge(String status) { final (Color color, IconData icon) = switch (status) { 'Actif' => (AppColors.success, Icons.check_circle_outline), 'Inactif' => (AppColors.error, Icons.cancel_outlined), 'En attente' => (AppColors.warning, Icons.schedule_outlined), 'Suspendu' => (AppColors.textTertiary, Icons.block_outlined), _ => (AppColors.textTertiary, Icons.help_outline), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 10, color: color), const SizedBox(width: 3), Text(status, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color)), ], ), ); } // ─── Empty state enrichi ────────────────────────────────────────────────── Widget _buildEmptyState(BuildContext context) { final hasFilters = _filterStatus != 'Tous' || _searchQuery.isNotEmpty; return Center( child: Padding( padding: const EdgeInsets.all(40), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration(color: ModuleColors.membres.withOpacity(0.08), shape: BoxShape.circle), child: Icon( hasFilters ? Icons.filter_list_off : Icons.people_outline, size: 44, color: ModuleColors.membres, ), ), const SizedBox(height: 20), Text( hasFilters ? 'Aucun résultat' : 'Aucun membre', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: 8), Text( hasFilters ? 'Aucun membre ne correspond à votre recherche ou filtre.' : 'Aucun membre enregistré pour le moment.', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, height: 1.5, color: Theme.of(context).colorScheme.onSurfaceVariant), ), const SizedBox(height: 24), if (hasFilters) FilledButton.icon( onPressed: () { setState(() { _filterStatus = 'Tous'; _searchQuery = ''; _searchController.clear(); }); widget.onSearch?.call(null); }, icon: const Icon(Icons.filter_list_off, size: 18), label: const Text('Effacer les filtres'), style: FilledButton.styleFrom(backgroundColor: ModuleColors.membres), ) else if (widget.onAddMember != null) FilledButton.icon( onPressed: widget.onAddMember, icon: const Icon(Icons.person_add_outlined, size: 18), label: const Text('Ajouter un membre'), style: FilledButton.styleFrom(backgroundColor: ModuleColors.membres), ), ], ), ), ); } // ─── Navigation vers page de détail ────────────────────────────────────── void _navigateToDetail(BuildContext context, Map member) { Navigator.of(context).push(MaterialPageRoute( builder: (_) => MemberDetailPage( member: member, isSuperAdmin: _isSuperAdmin, onActivateMember: widget.onActivateMember, onResetPassword: widget.onResetPassword, onAffecterOrganisation: widget.onAffecterOrganisation, onLifecycleAction: widget.onLifecycleAction, onDeleteAccount: widget.onDeleteAccount, organisationsPicker: _organisationsPicker, onOrganisationsLoaded: (orgs) => setState(() => _organisationsPicker = orgs), ), )); } // ─── Bulk actions ───────────────────────────────────────────────────────── Future _showBulkAffecterDialog() async { await _loadOrganisationsPicker(); if (!mounted) return; String? selectedOrgId; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, set) => AlertDialog( title: Text('Affecter ${_selectedIds.length} membre(s)'), content: _organisationsPicker.isEmpty ? const Text('Aucune organisation disponible.') : DropdownButtonFormField( decoration: const InputDecoration(labelText: 'Organisation'), items: _organisationsPicker .map((o) => DropdownMenuItem(value: o['id'], child: Text(o['nom']!))) .toList(), onChanged: (v) => set(() => selectedOrgId = v), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.membres, foregroundColor: AppColors.onPrimary), onPressed: selectedOrgId == null ? null : () { Navigator.pop(ctx); for (final id in _selectedIds) { widget.onAffecterOrganisation!(id, selectedOrgId!); } _exitSelectionMode(); }, child: const Text('Confirmer'), ), ], ), ), ); } void _bulkExporter() { final count = _selectedIds.length; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Export de $count membre(s) — fonctionnalité à venir'), backgroundColor: ModuleColors.membres, duration: const Duration(seconds: 3), ), ); _exitSelectionMode(); } Future _showAffecterOrganisationDialog(BuildContext ctx, Map member) async { await _loadOrganisationsPicker(); if (!mounted) return; String? selectedOrgId; await showDialog( context: ctx, builder: (dialogCtx) => StatefulBuilder( builder: (dialogCtx, set) => AlertDialog( title: const Text('Affecter à une organisation'), content: _organisationsPicker.isEmpty ? const Text('Aucune organisation disponible.') : DropdownButtonFormField( decoration: const InputDecoration(labelText: 'Organisation'), items: _organisationsPicker .map((o) => DropdownMenuItem(value: o['id'], child: Text(o['nom']!))) .toList(), onChanged: (v) => set(() => selectedOrgId = v), ), actions: [ TextButton(onPressed: () => Navigator.pop(dialogCtx), child: const Text('Annuler')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.membres, foregroundColor: AppColors.onPrimary), onPressed: selectedOrgId == null ? null : () { Navigator.pop(dialogCtx); widget.onAffecterOrganisation!(member['id'] as String, selectedOrgId!); }, child: const Text('Confirmer'), ), ], ), ), ); } Future _loadOrganisationsPicker() async { if (_organisationsPicker.isNotEmpty) return; try { final repo = GetIt.instance(); final orgs = await repo.getOrganizations(page: 0, size: 100); if (mounted) { setState(() { _organisationsPicker = orgs .where((o) => o.id != null && o.id!.isNotEmpty) .map((o) => {'id': o.id!, 'nom': o.nomAffichage}) .toList(); }); } } catch (_) {} } } // ═══════════════════════════════════════════════════════════════════════════════ // Page de détail membre (navigation complète) // ═══════════════════════════════════════════════════════════════════════════════ class MemberDetailPage extends StatefulWidget { final Map member; final bool isSuperAdmin; final void Function(String memberId)? onActivateMember; final void Function(String memberId)? onResetPassword; final void Function(String memberId, String organisationId)? onAffecterOrganisation; final void Function(String memberId, String organisationId, String action, String? motif)? onLifecycleAction; final void Function(String memberId)? onDeleteAccount; final List> organisationsPicker; final void Function(List> orgs) onOrganisationsLoaded; const MemberDetailPage({ super.key, required this.member, required this.isSuperAdmin, this.onActivateMember, this.onResetPassword, this.onAffecterOrganisation, this.onLifecycleAction, this.onDeleteAccount, required this.organisationsPicker, required this.onOrganisationsLoaded, }); @override State createState() => _MemberDetailPageState(); } class _MemberDetailPageState extends State { late List> _orgs; @override void initState() { super.initState(); _orgs = List.from(widget.organisationsPicker); } @override Widget build(BuildContext context) { final m = widget.member; final name = m['name'] as String? ?? ''; final role = m['role'] as String? ?? 'Membre'; final status = m['status'] as String? ?? '?'; final numero = m['numeroMembre'] as String?; final email = m['email'] as String? ?? '—'; final phone = m['phone'] as String? ?? ''; final orgNom = m['organisationNom'] as String?; final dateAdhesion = m['joinDate']; final profession = m['department'] as String? ?? ''; final nationalite = m['nationalite'] as String? ?? ''; final location = (m['location'] as String? ?? '').trim(); final cotisAJour = m['cotisationAJour'] as bool? ?? false; final statutKyc = m['statutKyc'] as String?; final membreBureau = m['membreBureau'] as bool? ?? false; final responsable = m['responsable'] as bool? ?? false; final fonctionBureau = m['fonctionBureau'] as String?; final eventsCount = m['eventsAttended'] as int? ?? 0; return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: CustomScrollView( slivers: [ // ── Hero AppBar violet ─────────────────────────────────────────── SliverAppBar( expandedHeight: 210, pinned: true, backgroundColor: ModuleColors.membres, foregroundColor: AppColors.onPrimary, iconTheme: const IconThemeData(color: Colors.white), actionsIconTheme: const IconThemeData(color: Colors.white), actions: [ if (widget.onResetPassword != null) IconButton( icon: const Icon(Icons.lock_reset_outlined), tooltip: 'Réinitialiser le mot de passe', onPressed: () { Navigator.pop(context); widget.onResetPassword!(m['id'] as String); }, ), const SizedBox(width: 4), ], flexibleSpace: FlexibleSpaceBar( background: Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ModuleColors.membresDark, ModuleColors.membres], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 32), Container( width: 74, height: 74, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white.withOpacity(0.45), width: 2.5), gradient: const LinearGradient( colors: [ModuleColors.membresDark, ModuleColors.membres], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), alignment: Alignment.center, child: Text( m['initiales'] as String? ?? '??', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 28), ), ), const SizedBox(height: 10), Text(name, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700)), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(role, style: TextStyle(color: Colors.white.withOpacity(0.85), fontSize: 13)), if (numero != null && numero.isNotEmpty) ...[ Text(' · ', style: TextStyle(color: Colors.white.withOpacity(0.5))), Text(numero, style: TextStyle(color: Colors.white.withOpacity(0.75), fontSize: 12)), ], ], ), ], ), ), ), ), ), // ── Contenu ────────────────────────────────────────────────────── SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Badges statut / cotisation / KYC Wrap( spacing: 8, runSpacing: 6, children: [ _detailStatusBadge(context, status), _badgeChip( context, cotisAJour ? Icons.payments_outlined : Icons.money_off_outlined, cotisAJour ? AppColors.success : AppColors.error, cotisAJour ? 'Cotisation OK' : 'Cotisation en retard', ), if (statutKyc != null) _kycBadge(context, statutKyc), if (membreBureau) _badgeChip(context, Icons.star_outline, AppColors.warningUI, fonctionBureau ?? 'Bureau'), if (responsable) _badgeChip(context, Icons.manage_accounts_outlined, ModuleColors.membres, 'Responsable'), ], ), const SizedBox(height: 20), // Contact _sectionCard(context, 'Contact', [ _detailRow(context, Icons.email_outlined, 'Email', email), if (phone.isNotEmpty) _detailRow(context, Icons.phone_outlined, 'Téléphone', phone), ]), const SizedBox(height: 12), // Organisation if (orgNom != null || dateAdhesion != null || eventsCount > 0) _sectionCard(context, 'Organisation', [ if (orgNom != null) _detailRow(context, Icons.business_outlined, 'Organisation', orgNom), if (dateAdhesion != null) _detailRow(context, Icons.calendar_today_outlined, 'Adhésion', _fmt(dateAdhesion)), if (eventsCount > 0) _detailRow(context, Icons.event_outlined, 'Événements participés', eventsCount.toString()), ]), if (orgNom != null || dateAdhesion != null || eventsCount > 0) const SizedBox(height: 12), // Profil if (profession.isNotEmpty || nationalite.isNotEmpty || (location != ',' && location.isNotEmpty)) _sectionCard(context, 'Profil', [ if (profession.isNotEmpty) _detailRow(context, Icons.work_outline, 'Profession', profession), if (nationalite.isNotEmpty) _detailRow(context, Icons.flag_outlined, 'Nationalité', nationalite), if (location.isNotEmpty && location != ',') _detailRow(context, Icons.location_on_outlined, 'Localisation', location), ]), if (profession.isNotEmpty || nationalite.isNotEmpty || (location != ',' && location.isNotEmpty)) const SizedBox(height: 20), // Actions _sectionLabel(context, 'Actions'), const SizedBox(height: 10), if (status == 'En attente' && widget.onActivateMember != null) _actionBtn(context, 'Activer le membre', Icons.check_circle_outline, AppColors.success, () { Navigator.pop(context); widget.onActivateMember!(m['id'] as String); }), if (widget.onResetPassword != null) _actionBtn(context, 'Réinitialiser le mot de passe', Icons.lock_reset_outlined, AppColors.warning, () { Navigator.pop(context); widget.onResetPassword!(m['id'] as String); }, outlined: true), if (widget.isSuperAdmin && widget.onAffecterOrganisation != null && (m['organisationId'] == null || (m['organisationId'] as String).isEmpty)) _actionBtn(context, 'Affecter à une organisation', Icons.business_outlined, ModuleColors.membres, () => _showAffecterDialog(context), outlined: true), if (widget.onLifecycleAction != null) ..._lifecycleButtons(context, m), // Suppression définitive — SuperAdmin uniquement, ET membre encore actif if (widget.isSuperAdmin && widget.onDeleteAccount != null && (m['actif'] as bool? ?? true)) ...[ const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 12), _actionBtn( context, 'Supprimer ce compte', Icons.delete_forever_outlined, AppColors.error, () => _confirmDeleteAccount(context, m), outlined: true, ), ], // Bannière "Compte désactivé" si le membre a été désactivé if ((m['actif'] as bool? ?? true) == false) ...[ const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.error.withOpacity( Theme.of(context).brightness == Brightness.dark ? 0.15 : 0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.error.withOpacity(0.3)), ), child: Row( children: [ const Icon(Icons.block_outlined, color: AppColors.error, size: 18), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Compte désactivé', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.error), ), Text( 'Ce compte a été supprimé et ne peut plus se connecter.', style: TextStyle( fontSize: 11, color: Theme.of(context).brightness == Brightness.dark ? AppColors.textSecondaryDark : AppColors.textSecondary), ), ], ), ), ], ), ), ], ], ), ), ), ], ), ); } // ── Helpers UI ───────────────────────────────────────────────────────────── Widget _sectionCard(BuildContext context, String label, List children) { if (children.isEmpty) return const SizedBox.shrink(); return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1), ), padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionLabel(context, label), const SizedBox(height: 10), ...children, ], ), ); } Widget _sectionLabel(BuildContext context, String text) { return Text( text.toUpperCase(), style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: ModuleColors.membres, letterSpacing: 0.8), ); } Widget _detailRow(BuildContext context, IconData icon, String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 16, color: ModuleColors.membres), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: 11, color: Theme.of(context).colorScheme.onSurfaceVariant)), const SizedBox(height: 2), Text(value, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w500)), ], ), ), ], ), ); } Widget _badgeChip(BuildContext context, IconData icon, Color color, String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.35), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: color), const SizedBox(width: 5), Text(label, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)), ], ), ); } Widget _kycBadge(BuildContext context, String statutKyc) { final (Color color, IconData icon, String label) = switch (statutKyc) { 'VERIFIE' => (AppColors.success, Icons.verified_user_outlined, 'KYC Vérifié'), 'EN_COURS' => (AppColors.warning, Icons.pending_outlined, 'KYC En cours'), 'REJETE' => (AppColors.error, Icons.gpp_bad_outlined, 'KYC Rejeté'), _ => (AppColors.textTertiary, Icons.help_outline, 'KYC Inconnu'), }; return _badgeChip(context, icon, color, label); } Widget _detailStatusBadge(BuildContext context, String status) { final (Color color, IconData icon) = switch (status) { 'Actif' => (AppColors.success, Icons.check_circle_outline), 'Inactif' => (AppColors.error, Icons.cancel_outlined), 'En attente' => (AppColors.warning, Icons.schedule_outlined), 'Suspendu' => (AppColors.textTertiary, Icons.block_outlined), _ => (AppColors.textTertiary, Icons.help_outline), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.4), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color), const SizedBox(width: 5), Text(status, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: color)), ], ), ); } Widget _actionBtn(BuildContext context, String label, IconData icon, Color color, VoidCallback onPressed, {bool outlined = false}) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: SizedBox( width: double.infinity, child: outlined ? OutlinedButton.icon( onPressed: onPressed, icon: Icon(icon, size: 18), label: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: OutlinedButton.styleFrom( foregroundColor: color, side: BorderSide(color: color), padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ) : ElevatedButton.icon( onPressed: onPressed, icon: Icon(icon, size: 18), label: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: AppColors.onPrimary, padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ); } List _lifecycleButtons(BuildContext context, Map m) { final memberId = m['id'] as String? ?? ''; final statut = m['statutMembre'] as String? ?? m['status'] as String? ?? ''; // Organisation du membre — utilisée pour les actions lifecycle. // En contexte orgAdmin, c'est l'org fixe ; en superAdmin, on prend l'org du membre. final orgId = m['organisationId'] as String? ?? ''; // Un orgAdmin ne peut pas agir sur un autre admin ou super-admin. // Le superAdmin peut agir sur tout le monde — pas de restriction. final roleCode = (m['roleCode'] as String? ?? '').toUpperCase(); final cibleEstAdmin = roleCode == 'ORGADMIN' || roleCode == 'SUPERADMIN' || roleCode == 'ADMIN_ORGANISATION' || roleCode == 'SUPER_ADMIN'; if (cibleEstAdmin && !widget.isSuperAdmin) return const []; // Si on n'a pas d'org pour ce membre, impossible d'envoyer les actions lifecycle if (orgId.isEmpty) return const []; return [ if (statut == 'INVITE' || statut == 'EN_ATTENTE_VALIDATION' || statut == 'En attente') _actionBtn(context, "Activer l'adhésion", Icons.check_circle_outline, AppColors.success, () { Navigator.pop(context); widget.onLifecycleAction!(memberId, orgId, 'activer', null); }), if (statut == 'ACTIF' || statut == 'Actif') _actionBtn(context, "Suspendre l'adhésion", Icons.pause_circle_outline, AppColors.warning, () { _showMotifDialog(context, "Suspendre l'adhésion", onConfirm: (motif) => widget.onLifecycleAction!(memberId, orgId, 'suspendre', motif)); }, outlined: true), if (statut == 'SUSPENDU' || statut == 'Suspendu') _actionBtn(context, "Réactiver l'adhésion", Icons.play_circle_outline, AppColors.info, () { Navigator.pop(context); widget.onLifecycleAction!(memberId, orgId, 'activer', null); }), if (statut != 'RADIE' && statut != 'ARCHIVE' && statut.isNotEmpty) _actionBtn(context, "Radier de l'organisation", Icons.block_outlined, AppColors.error, () { _showMotifDialog(context, 'Radier le membre', onConfirm: (motif) => widget.onLifecycleAction!(memberId, orgId, 'radier', motif)); }, outlined: true), ]; } void _showMotifDialog(BuildContext context, String titre, {required void Function(String? motif) onConfirm}) { final ctrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(titre), content: TextField( controller: ctrl, decoration: const InputDecoration(labelText: 'Motif (optionnel)', border: OutlineInputBorder()), maxLines: 3, ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.membres, foregroundColor: AppColors.onPrimary), onPressed: () { Navigator.pop(ctx); Navigator.pop(context); onConfirm(ctrl.text.isNotEmpty ? ctrl.text : null); }, child: const Text('Confirmer'), ), ], ), ); } /// Confirmation de suppression définitive (SuperAdmin only). /// Protection anti-clic — double confirmation + saisie du nom. Future _confirmDeleteAccount(BuildContext context, Map m) async { final isDark = Theme.of(context).brightness == Brightness.dark; final name = (m['name'] as String? ?? '').trim(); final memberId = m['id'] as String? ?? ''; final roleCode = (m['roleCode'] as String? ?? '').toUpperCase(); final isAdminTarget = roleCode == 'ORGADMIN' || roleCode == 'ADMIN_ORGANISATION'; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondary; final confirmed = await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), backgroundColor: isDark ? AppColors.surfaceDark : AppColors.surface, title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.error.withOpacity(isDark ? 0.2 : 0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.warning_amber_rounded, color: AppColors.error, size: 22), ), const SizedBox(width: 10), Expanded( child: Text( 'Supprimer ce compte ?', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: textPrimary), ), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Vous êtes sur le point de supprimer le compte de :', style: TextStyle(fontSize: 13, color: textSecondary), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColors.error.withOpacity(isDark ? 0.12 : 0.06), borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.error.withOpacity(0.3)), ), child: Row( children: [ if (isAdminTarget) ...[ Icon(Icons.admin_panel_settings_outlined, size: 16, color: AppColors.error), const SizedBox(width: 6), ], Expanded( child: Text( name.isEmpty ? '(sans nom)' : name, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: textPrimary), ), ), ], ), ), const SizedBox(height: 12), Text( isAdminTarget ? 'Ce membre est administrateur d\'organisation. Sa suppression désactivera son compte et suspendra toutes ses adhésions.' : 'Le compte sera désactivé et toutes ses adhésions seront suspendues. Cette action est irréversible.', style: TextStyle(fontSize: 12, color: textSecondary, height: 1.4), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: Text('Annuler', style: TextStyle(color: textSecondary, fontWeight: FontWeight.w600)), ), ElevatedButton.icon( onPressed: () => Navigator.of(ctx).pop(true), icon: const Icon(Icons.delete_forever, size: 18), label: const Text('Supprimer', style: TextStyle(fontWeight: FontWeight.w700)), style: ElevatedButton.styleFrom( backgroundColor: AppColors.error, foregroundColor: AppColors.onError, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), ), ), ], ), ); if (confirmed == true && context.mounted) { // Fermer l'action sheet en plus du dialog Navigator.of(context).pop(); widget.onDeleteAccount!(memberId); } } Future _showAffecterDialog(BuildContext context) async { if (_orgs.isEmpty) { try { final repo = GetIt.instance(); final orgs = await repo.getOrganizations(page: 0, size: 100); if (mounted) { setState(() { _orgs = orgs.where((o) => o.id != null && o.id!.isNotEmpty).map((o) => {'id': o.id!, 'nom': o.nomAffichage}).toList(); widget.onOrganisationsLoaded(_orgs); }); } } catch (_) {} } if (!mounted) return; String? selectedOrgId; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, set) => AlertDialog( title: const Text('Affecter à une organisation'), content: _orgs.isEmpty ? const Text('Aucune organisation disponible.') : DropdownButtonFormField( decoration: const InputDecoration(labelText: 'Organisation'), items: _orgs.map((o) => DropdownMenuItem(value: o['id'], child: Text(o['nom']!))).toList(), onChanged: (v) => set(() => selectedOrgId = v), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annuler')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: ModuleColors.membres, foregroundColor: AppColors.onPrimary), onPressed: selectedOrgId == null ? null : () { Navigator.pop(ctx); Navigator.pop(context); widget.onAffecterOrganisation!(widget.member['id'] as String, selectedOrgId!); }, child: const Text('Confirmer'), ), ], ), ), ); } String _fmt(dynamic date) { if (date == null) return '—'; if (date is DateTime) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } return date.toString(); } } /// Données d'une action swipe gauche pour les cartes membres class _SwipeAction { final String action; // 'activer' | 'suspendre' | 'radier' final IconData icon; final String label; final Color color; const _SwipeAction({ required this.action, required this.icon, required this.label, required this.color, }); }