From 14ecdd642bd1dadcf3ad4d90732c9e18467eabd5 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:05:07 +0000 Subject: [PATCH] =?UTF-8?q?feat(members):=20filtres=20toujours=20visibles?= =?UTF-8?q?=20+=20filtre=20par=20r=C3=B4le=20+=20refresh=20AppBar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Améliorations UX Annuaire Membres (parité avec Gestion Organisations) : 1. Panneau recherche/filtres toujours visible (plus de toggle collapsible) → UX plus directe, pas de clic supplémentaire pour chercher 2. Filtre par rôle (nouveau) : chips Admin, Modérateur, Membre Actif, Membre Simple avec icône badge devant la rangée, couleur accent différente du statut 3. Bouton Refresh ajouté dans l'AppBar (cohérence avec la page Organisations) 4. Lien 'Réinitialiser les filtres' visible quand au moins un filtre est actif 5. _filteredMembers intègre le filtre rôle via _roleFilterMapping Supprimé : - _isSearchExpanded (plus utile — panneau toujours visible) - Bouton toggle search dans l'AppBar (remplacé par affichage permanent) --- .../pages/members_page_connected.dart | 134 ++++++++++++------ 1 file changed, 89 insertions(+), 45 deletions(-) diff --git a/lib/features/members/presentation/pages/members_page_connected.dart b/lib/features/members/presentation/pages/members_page_connected.dart index 6766d5d..64d85f4 100644 --- a/lib/features/members/presentation/pages/members_page_connected.dart +++ b/lib/features/members/presentation/pages/members_page_connected.dart @@ -63,9 +63,9 @@ class _MembersPageState extends State { final ScrollController _scrollController = ScrollController(); String _searchQuery = ''; String _filterStatus = 'Tous'; + String _filterRole = 'Tous'; Timer? _searchDebounce; - bool _isSearchExpanded = false; bool _isGridView = false; bool _isSelectionMode = false; final Set _selectedIds = {}; @@ -144,6 +144,13 @@ class _MembersPageState extends State { }); } + static const _roleFilterMapping = { + 'Admin': 'Administrateur Org', + 'Modérateur': 'Modérateur', + 'Membre Actif': 'Membre Actif', + 'Membre Simple': 'Membre Simple', + }; + List> get _filteredMembers { return _accumulatedMembers.where((m) { final matchesSearch = _searchQuery.isEmpty || @@ -151,7 +158,9 @@ class _MembersPageState extends State { (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; + final matchesRole = _filterRole == 'Tous' || + (m['role'] as String? ?? '') == _roleFilterMapping[_filterRole]; + return matchesSearch && matchesStatus && matchesRole; }).toList(); } @@ -167,15 +176,7 @@ class _MembersPageState extends State { 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), - ), + _buildSearchAndFilters(context), Expanded(child: _buildContent(context)), ], ), @@ -185,30 +186,10 @@ class _MembersPageState extends State { // ─── 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), @@ -220,6 +201,14 @@ class _MembersPageState extends State { onPressed: widget.onAddMember, tooltip: 'Ajouter un membre', ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() => _accumulatedMembers.clear()); + widget.onRefresh(); + }, + tooltip: 'Rafraîchir', + ), const SizedBox(width: 4), ], ); @@ -337,9 +326,10 @@ class _MembersPageState extends State { Widget _kpiSeparator(BuildContext context) => Container(width: 1, height: 42, color: Theme.of(context).colorScheme.outlineVariant); - // ─── Panneau recherche collapsible ──────────────────────────────────────── + // ─── Recherche + Filtres (toujours visibles) ──────────────────────────── - Widget _buildSearchPanel(BuildContext context) { + Widget _buildSearchAndFilters(BuildContext context) { + final hasFilter = _filterStatus != 'Tous' || _filterRole != 'Tous' || _searchQuery.isNotEmpty; return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), decoration: BoxDecoration( @@ -348,7 +338,9 @@ class _MembersPageState extends State { ), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Barre de recherche TextField( controller: _searchController, onChanged: (v) { @@ -364,10 +356,7 @@ class _MembersPageState extends State { 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, - ) + ? 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)), @@ -378,33 +367,88 @@ class _MembersPageState extends State { ), ), const SizedBox(height: 8), + + // Chips statut 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), + child: _buildFilterChip(context, f, isStatus: true), )) .toList(), ), ), + const SizedBox(height: 6), + + // Chips rôle + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 6), + child: Icon(Icons.badge_outlined, size: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ...['Tous', 'Admin', 'Modérateur', 'Membre Actif', 'Membre Simple'] + .map((f) => Padding( + padding: const EdgeInsets.only(right: 8), + child: _buildFilterChip(context, f, isStatus: false), + )) + .toList(), + ], + ), + ), + + // Lien réinitialiser + if (hasFilter) + Padding( + padding: const EdgeInsets.only(top: 6), + child: GestureDetector( + onTap: () { + setState(() { + _filterStatus = 'Tous'; + _filterRole = 'Tous'; + _searchQuery = ''; + _searchController.clear(); + }); + widget.onSearch?.call(null); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.filter_list_off, size: 13, color: ModuleColors.membres), + const SizedBox(width: 4), + Text('Réinitialiser les filtres', + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: ModuleColors.membres)), + ], + ), + ), + ), ], ), ); } - Widget _buildFilterChip(BuildContext context, String label) { - final isSelected = _filterStatus == label; + Widget _buildFilterChip(BuildContext context, String label, {bool isStatus = true}) { + final isSelected = isStatus ? _filterStatus == label : _filterRole == label; + final accentColor = isStatus ? ModuleColors.membres : ModuleColors.organisationsDark; return GestureDetector( - onTap: () => setState(() => _filterStatus = label), + onTap: () => setState(() { + if (isStatus) { + _filterStatus = label; + } else { + _filterRole = label; + } + }), child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: isSelected ? ModuleColors.membres : Colors.transparent, + color: isSelected ? accentColor : Colors.transparent, borderRadius: BorderRadius.circular(20), - border: Border.all(color: isSelected ? ModuleColors.membres : Theme.of(context).colorScheme.outline, width: 1), + border: Border.all(color: isSelected ? accentColor : Theme.of(context).colorScheme.outline, width: 1), ), child: Text( label, @@ -926,7 +970,7 @@ class _MembersPageState extends State { // ─── Empty state enrichi ────────────────────────────────────────────────── Widget _buildEmptyState(BuildContext context) { - final hasFilters = _filterStatus != 'Tous' || _searchQuery.isNotEmpty; + final hasFilters = _filterStatus != 'Tous' || _filterRole != 'Tous' || _searchQuery.isNotEmpty; return Center( child: Padding( padding: const EdgeInsets.all(40), @@ -959,7 +1003,7 @@ class _MembersPageState extends State { if (hasFilters) FilledButton.icon( onPressed: () { - setState(() { _filterStatus = 'Tous'; _searchQuery = ''; _searchController.clear(); }); + setState(() { _filterStatus = 'Tous'; _filterRole = 'Tous'; _searchQuery = ''; _searchController.clear(); }); widget.onSearch?.call(null); }, icon: const Icon(Icons.filter_list_off, size: 18),