/// Page des membres avec données injectées depuis le BLoC /// /// Cette version de MembersPage accepte les données en paramètre /// au lieu d'utiliser des données mock hardcodées. library members_page_connected; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/auth/bloc/auth_bloc.dart'; import '../../../../core/auth/models/user_role.dart'; import '../../../../core/utils/logger.dart'; import '../widgets/add_member_dialog.dart'; import '../../bloc/membres_bloc.dart'; import '../../bloc/membres_event.dart'; /// Page de gestion des membres avec données injectées class MembersPageWithData extends StatefulWidget { /// Liste des membres à afficher final List> members; /// Nombre total de membres (pour la pagination) final int totalCount; /// Page actuelle final int currentPage; /// Nombre total de pages final int totalPages; /// Taille de la page final int pageSize; const MembersPageWithData({ super.key, required this.members, required this.totalCount, required this.currentPage, required this.totalPages, this.pageSize = 20, }); @override State createState() => _MembersPageWithDataState(); } class _MembersPageWithDataState extends State with TickerProviderStateMixin { // Controllers et état final TextEditingController _searchController = TextEditingController(); late TabController _tabController; // État de l'interface String _searchQuery = ''; String _selectedFilter = 'Tous'; final String _selectedSort = 'Nom'; bool _isGridView = false; bool _showAdvancedFilters = false; // Filtres avancés final List _selectedRoles = []; List _selectedStatuses = ['Actif', 'Inactif', 'Suspendu', 'En attente']; DateTimeRange? _dateRange; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); AppLogger.info('MembersPageWithData initialisée avec ${widget.members.length} membres'); } @override void dispose() { _searchController.dispose(); _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is! AuthAuthenticated) { return Container( color: const Color(0xFFF8F9FA), child: const Center(child: CircularProgressIndicator()), ); } return Container( color: const Color(0xFFF8F9FA), child: _buildMembersContent(state), ); }, ); } /// Contenu principal de la page membres Widget _buildMembersContent(AuthAuthenticated state) { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header avec titre et actions _buildMembersHeader(state), const SizedBox(height: 16), // Statistiques et métriques _buildMembersMetrics(), const SizedBox(height: 16), // Barre de recherche et filtres _buildSearchAndFilters(), const SizedBox(height: 16), // Onglets de catégories _buildCategoryTabs(), const SizedBox(height: 16), // Liste/Grille des membres _buildMembersDisplay(), // Pagination if (widget.totalPages > 1) ...[ const SizedBox(height: 16), _buildPagination(), ], ], ), ); } /// Header avec titre et actions principales Widget _buildMembersHeader(AuthAuthenticated state) { final canManageMembers = _canManageMembers(state.effectiveRole); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: const Color(0xFF6C5CE7).withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.people, color: Colors.white, size: 28, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gestion des Membres', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( '${widget.totalCount} membres au total', style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 14, ), ), ], ), ), if (canManageMembers) ...[ IconButton( icon: const Icon(Icons.add_circle, color: Colors.white, size: 28), onPressed: () { AppLogger.userAction('Add new member button clicked'); _showAddMemberDialog(); }, tooltip: 'Ajouter un membre', ), IconButton( icon: const Icon(Icons.file_download, color: Colors.white), onPressed: () { AppLogger.userAction('Export members button clicked'); _exportMembers(); }, tooltip: 'Exporter', ), ], ], ), ], ), ); } /// Métriques et statistiques des membres Widget _buildMembersMetrics() { final filteredMembers = _getFilteredMembers(); final activeMembers = filteredMembers.where((m) => m['status'] == 'Actif').length; final inactiveMembers = filteredMembers.where((m) => m['status'] == 'Inactif').length; final pendingMembers = filteredMembers.where((m) => m['status'] == 'En attente').length; return Row( children: [ Expanded( child: _buildMetricCard( 'Actifs', activeMembers.toString(), Icons.check_circle, const Color(0xFF00B894), ), ), const SizedBox(width: 12), Expanded( child: _buildMetricCard( 'Inactifs', inactiveMembers.toString(), Icons.pause_circle, const Color(0xFFFFBE76), ), ), const SizedBox(width: 12), Expanded( child: _buildMetricCard( 'En attente', pendingMembers.toString(), Icons.pending, const Color(0xFF74B9FF), ), ), ], ); } /// Carte de métrique individuelle Widget _buildMetricCard(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( children: [ Icon(icon, color: color, size: 32), const SizedBox(height: 8), Text( value, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 4), Text( label, style: const TextStyle( fontSize: 12, color: Color(0xFF636E72), ), ), ], ), ); } /// Barre de recherche et filtres Widget _buildSearchAndFilters() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( children: [ Row( children: [ Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher un membre...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), filled: true, fillColor: const Color(0xFFF8F9FA), ), onChanged: (value) { setState(() { _searchQuery = value; }); AppLogger.userAction('Search members', data: {'query': value}); }, ), ), const SizedBox(width: 12), IconButton( icon: Icon( _isGridView ? Icons.view_list : Icons.grid_view, color: const Color(0xFF6C5CE7), ), onPressed: () { setState(() { _isGridView = !_isGridView; }); AppLogger.userAction('Toggle view mode', data: {'isGrid': _isGridView}); }, tooltip: _isGridView ? 'Vue liste' : 'Vue grille', ), ], ), ], ), ); } /// Onglets de catégories Widget _buildCategoryTabs() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: TabBar( controller: _tabController, labelColor: const Color(0xFF6C5CE7), unselectedLabelColor: const Color(0xFF636E72), indicatorColor: const Color(0xFF6C5CE7), tabs: const [ Tab(text: 'Tous'), Tab(text: 'Actifs'), Tab(text: 'Équipes'), Tab(text: 'Analytics'), ], ), ); } /// Affichage principal des membres Widget _buildMembersDisplay() { final filteredMembers = _getFilteredMembers(); if (filteredMembers.isEmpty) { return _buildEmptyState(); } return SizedBox( height: 600, child: TabBarView( controller: _tabController, children: [ _buildMembersList(filteredMembers), _buildMembersList(filteredMembers.where((m) => m['status'] == 'Actif').toList()), _buildTeamsView(filteredMembers), _buildAnalyticsView(filteredMembers), ], ), ); } /// Liste des membres Widget _buildMembersList(List> members) { if (_isGridView) { return _buildMembersGrid(members); } return ListView.builder( itemCount: members.length, padding: const EdgeInsets.all(8), itemBuilder: (context, index) { final member = members[index]; return _buildMemberCard(member); }, ); } /// Carte d'un membre Widget _buildMemberCard(Map member) { return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: ListTile( leading: CircleAvatar( backgroundColor: const Color(0xFF6C5CE7), child: Text( _getInitials(member['name']), style: const TextStyle(color: Colors.white), ), ), title: Text( member['name'], style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text(member['email']), trailing: _buildStatusChip(member['status']), onTap: () { AppLogger.userAction('View member details', data: {'memberId': member['id']}); _showMemberDetails(member); }, ), ); } /// Grille des membres Widget _buildMembersGrid(List> members) { return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.85, ), itemCount: members.length, itemBuilder: (context, index) { final member = members[index]; return _buildMemberGridCard(member); }, ); } /// Carte membre pour la grille Widget _buildMemberGridCard(Map member) { return Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: InkWell( onTap: () { AppLogger.userAction('View member details (grid)', data: {'memberId': member['id']}); _showMemberDetails(member); }, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(12), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircleAvatar( radius: 30, backgroundColor: const Color(0xFF6C5CE7), child: Text( _getInitials(member['name']), style: const TextStyle(color: Colors.white, fontSize: 20), ), ), const SizedBox(height: 12), Text( member['name'], style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( member['role'], style: const TextStyle( fontSize: 12, color: Color(0xFF636E72), ), textAlign: TextAlign.center, ), const SizedBox(height: 8), _buildStatusChip(member['status']), ], ), ), ), ); } /// Chip de statut Widget _buildStatusChip(String status) { Color color; switch (status) { case 'Actif': color = const Color(0xFF00B894); break; case 'Inactif': color = const Color(0xFFFFBE76); break; case 'Suspendu': color = const Color(0xFFFF7675); break; case 'En attente': color = const Color(0xFF74B9FF); break; default: color = const Color(0xFF636E72); } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( status, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w500, ), ), ); } /// Vue des équipes (placeholder) Widget _buildTeamsView(List> members) { return const Center( child: Text('Vue des équipes - À implémenter'), ); } /// Vue analytics (placeholder) Widget _buildAnalyticsView(List> members) { return const Center( child: Text('Vue analytics - À implémenter'), ); } /// État vide Widget _buildEmptyState() { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people_outline, size: 64, color: Color(0xFF636E72)), SizedBox(height: 16), Text( 'Aucun membre trouvé', style: TextStyle(fontSize: 18, color: Color(0xFF636E72)), ), ], ), ); } /// Pagination Widget _buildPagination() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: widget.currentPage > 0 ? () { AppLogger.userAction('Previous page', data: {'page': widget.currentPage - 1}); context.read().add(LoadMembres( page: widget.currentPage - 1, size: widget.pageSize, )); } : null, ), Text('Page ${widget.currentPage + 1} / ${widget.totalPages}'), IconButton( icon: const Icon(Icons.chevron_right), onPressed: widget.currentPage < widget.totalPages - 1 ? () { AppLogger.userAction('Next page', data: {'page': widget.currentPage + 1}); context.read().add(LoadMembres( page: widget.currentPage + 1, size: widget.pageSize, )); } : null, ), ], ), ); } /// Obtenir les membres filtrés List> _getFilteredMembers() { var filtered = widget.members; // Filtrer par recherche if (_searchQuery.isNotEmpty) { filtered = filtered.where((m) { final name = m['name'].toString().toLowerCase(); final email = m['email'].toString().toLowerCase(); final query = _searchQuery.toLowerCase(); return name.contains(query) || email.contains(query); }).toList(); } // Filtrer par statut if (_selectedStatuses.isNotEmpty) { filtered = filtered.where((m) => _selectedStatuses.contains(m['status'])).toList(); } return filtered; } /// Obtenir les initiales d'un nom String _getInitials(String name) { final parts = name.split(' '); if (parts.length >= 2) { return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); } return name.substring(0, 1).toUpperCase(); } /// Vérifier si l'utilisateur peut gérer les membres bool _canManageMembers(UserRole role) { return role.level >= UserRole.moderator.level; } /// Afficher les détails d'un membre void _showMemberDetails(Map member) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(member['name']), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Email: ${member['email']}'), Text('Rôle: ${member['role']}'), Text('Statut: ${member['status']}'), if (member['phone'] != null) Text('Téléphone: ${member['phone']}'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Fermer'), ), ], ), ); } /// Afficher le dialogue d'ajout de membre void _showAddMemberDialog() { showDialog( context: context, builder: (context) => BlocProvider.value( value: context.read(), child: const AddMemberDialog(), ), ); } /// Exporter les membres void _exportMembers() { // TODO: Implémenter l'export des membres ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Export des membres en cours...'), backgroundColor: Colors.blue, ), ); } } /// Version améliorée de MembersPageWithData avec support de la pagination class MembersPageWithDataAndPagination extends StatefulWidget { final List> members; final int totalCount; final int currentPage; final int totalPages; final Function(int page) onPageChanged; final VoidCallback onRefresh; const MembersPageWithDataAndPagination({ super.key, required this.members, required this.totalCount, required this.currentPage, required this.totalPages, required this.onPageChanged, required this.onRefresh, }); @override State createState() => _MembersPageWithDataAndPaginationState(); } class _MembersPageWithDataAndPaginationState extends State { final TextEditingController _searchController = TextEditingController(); late TabController _tabController; String _searchQuery = ''; String _selectedFilter = 'Tous'; bool _isGridView = false; final List _selectedRoles = []; List _selectedStatuses = ['Actif', 'Inactif', 'Suspendu', 'En attente']; @override void initState() { super.initState(); // Note: TabController nécessite un TickerProvider, on utilise un simple state sans mixin pour l'instant } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () async { widget.onRefresh(); // Attendre un peu pour l'animation await Future.delayed(const Duration(milliseconds: 500)); }, child: SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), const SizedBox(height: 16), _buildMetrics(), const SizedBox(height: 16), _buildMembersList(), if (widget.totalPages > 1) ...[ const SizedBox(height: 16), _buildPagination(), ], ], ), ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ const Icon(Icons.people, color: Colors.white, size: 28), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gestion des Membres', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), Text( '${widget.totalCount} membres au total', style: const TextStyle(color: Colors.white70, fontSize: 14), ), ], ), ), ], ), ); } Widget _buildMetrics() { final activeCount = widget.members.where((m) => m['status'] == 'Actif').length; final inactiveCount = widget.members.where((m) => m['status'] == 'Inactif').length; return Row( children: [ Expanded( child: _buildMetricCard('Actifs', activeCount.toString(), Icons.check_circle, const Color(0xFF00B894)), ), const SizedBox(width: 12), Expanded( child: _buildMetricCard('Inactifs', inactiveCount.toString(), Icons.pause_circle, const Color(0xFFFFBE76)), ), ], ); } Widget _buildMetricCard(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Icon(icon, color: color, size: 32), const SizedBox(height: 8), Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)), Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF636E72))), ], ), ); } Widget _buildMembersList() { if (widget.members.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(32.0), child: Text('Aucun membre trouvé'), ), ); } return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: widget.members.length, itemBuilder: (context, index) { final member = widget.members[index]; return Card( margin: const EdgeInsets.only(bottom: 12), child: ListTile( leading: CircleAvatar( backgroundColor: const Color(0xFF6C5CE7), child: Text( _getInitials(member['name']), style: const TextStyle(color: Colors.white), ), ), title: Text(member['name']), subtitle: Text(member['email']), trailing: _buildStatusChip(member['status']), ), ); }, ); } Widget _buildStatusChip(String status) { Color color; switch (status) { case 'Actif': color = const Color(0xFF00B894); break; case 'Inactif': color = const Color(0xFFFFBE76); break; default: color = const Color(0xFF636E72); } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( status, style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w500), ), ); } Widget _buildPagination() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: widget.currentPage > 0 ? () => widget.onPageChanged(widget.currentPage - 1) : null, ), Text('Page ${widget.currentPage + 1} / ${widget.totalPages}'), IconButton( icon: const Icon(Icons.chevron_right), onPressed: widget.currentPage < widget.totalPages - 1 ? () => widget.onPageChanged(widget.currentPage + 1) : null, ), ], ), ); } String _getInitials(String name) { final parts = name.split(' '); if (parts.length >= 2) { return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); } return name.substring(0, 1).toUpperCase(); } }