import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../features/authentication/presentation/bloc/auth_bloc.dart'; import '../../../../features/authentication/data/models/user_role.dart'; /// Page de gestion des membres - Interface sophistiquée et exhaustive /// /// Cette page offre une interface complète pour la gestion des membres /// avec des fonctionnalités avancées de recherche, filtrage, statistiques /// et actions de gestion basées sur les permissions utilisateur. class MembersPage extends StatefulWidget { const MembersPage({super.key}); @override State createState() => _MembersPageState(); } class _MembersPageState extends State with TickerProviderStateMixin { // Controllers et état final TextEditingController _searchController = TextEditingController(); late TabController _tabController; // État de l'interface String _searchQuery = ''; String _selectedFilter = 'Tous'; bool _isGridView = false; bool _showAdvancedFilters = false; // Filtres avancés final List _selectedRoles = []; List _selectedStatuses = ['Actif', 'Inactif', 'Suspendu', 'En attente']; DateTimeRange? _dateRange; // Données de démonstration enrichies final List> _allMembers = [ { 'id': '1', 'name': 'Marie Dubois', 'email': 'marie.dubois@unionflow.com', 'role': 'Membre Actif', 'status': 'Actif', 'joinDate': DateTime(2023, 1, 15), 'lastActivity': DateTime(2024, 9, 19), 'avatar': null, 'phone': '+33 6 12 34 56 78', 'department': 'Ressources Humaines', 'location': 'Paris, France', 'permissions': 15, 'contributionScore': 85, 'eventsAttended': 12, 'projectsInvolved': 5, }, { 'id': '2', 'name': 'Pierre Martin', 'email': 'pierre.martin@unionflow.com', 'role': 'Modérateur', 'status': 'Actif', 'joinDate': DateTime(2022, 11, 20), 'lastActivity': DateTime(2024, 9, 20), 'avatar': null, 'phone': '+33 6 98 76 54 32', 'department': 'IT & Développement', 'location': 'Lyon, France', 'permissions': 25, 'contributionScore': 92, 'eventsAttended': 18, 'projectsInvolved': 8, }, { 'id': '3', 'name': 'Sophie Laurent', 'email': 'sophie.laurent@unionflow.com', 'role': 'Membre Simple', 'status': 'Inactif', 'joinDate': DateTime(2024, 2, 10), 'lastActivity': DateTime(2024, 8, 15), 'avatar': null, 'phone': '+33 6 45 67 89 01', 'department': 'Marketing', 'location': 'Marseille, France', 'permissions': 8, 'contributionScore': 45, 'eventsAttended': 3, 'projectsInvolved': 1, }, { 'id': '4', 'name': 'Thomas Durand', 'email': 'thomas.durand@unionflow.com', 'role': 'Administrateur Org', 'status': 'Actif', 'joinDate': DateTime(2021, 6, 5), 'lastActivity': DateTime(2024, 9, 20), 'avatar': null, 'phone': '+33 6 23 45 67 89', 'department': 'Administration', 'location': 'Toulouse, France', 'permissions': 35, 'contributionScore': 98, 'eventsAttended': 25, 'projectsInvolved': 12, }, { 'id': '5', 'name': 'Emma Rousseau', 'email': 'emma.rousseau@unionflow.com', 'role': 'Gestionnaire RH', 'status': 'Actif', 'joinDate': DateTime(2023, 3, 12), 'lastActivity': DateTime(2024, 9, 19), 'avatar': null, 'phone': '+33 6 34 56 78 90', 'department': 'Ressources Humaines', 'location': 'Nantes, France', 'permissions': 28, 'contributionScore': 88, 'eventsAttended': 15, 'projectsInvolved': 7, }, { 'id': '6', 'name': 'Lucas Bernard', 'email': 'lucas.bernard@unionflow.com', 'role': 'Consultant', 'status': 'En attente', 'joinDate': DateTime(2024, 9, 1), 'lastActivity': DateTime(2024, 9, 18), 'avatar': null, 'phone': '+33 6 56 78 90 12', 'department': 'Consulting', 'location': 'Bordeaux, France', 'permissions': 12, 'contributionScore': 0, 'eventsAttended': 0, 'projectsInvolved': 0, }, { 'id': '7', 'name': 'Camille Moreau', 'email': 'camille.moreau@unionflow.com', 'role': 'Membre Actif', 'status': 'Suspendu', 'joinDate': DateTime(2022, 8, 30), 'lastActivity': DateTime(2024, 7, 10), 'avatar': null, 'phone': '+33 6 67 89 01 23', 'department': 'Ventes', 'location': 'Lille, France', 'permissions': 15, 'contributionScore': 65, 'eventsAttended': 8, 'projectsInvolved': 3, }, ]; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); } @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(), ], ), ); } /// Header avec titre et actions principales Widget _buildMembersHeader(AuthAuthenticated state) { final canManageMembers = _canManageMembers(state.effectiveRole); return UFPageHeader( title: 'Membres', icon: Icons.people, iconColor: ColorTokens.primary, actions: canManageMembers ? [ IconButton( icon: const Icon(Icons.checklist), onPressed: () => _showBulkActions(), tooltip: 'Actions groupées', ), IconButton( icon: const Icon(Icons.download), onPressed: () => _exportMembers(), tooltip: 'Exporter', ), IconButton( icon: const Icon(Icons.person_add), onPressed: () => _showAddMemberDialog(), tooltip: 'Ajouter un membre', ), ] : null, ); } /// Section des métriques et statistiques Widget _buildMembersMetrics() { final totalMembers = _allMembers.length; final activeMembers = _allMembers.where((m) => m['status'] == 'Actif').length; final newThisMonth = _allMembers.where((m) { final joinDate = m['joinDate'] as DateTime; final now = DateTime.now(); return joinDate.year == now.year && joinDate.month == now.month; }).length; final avgContribution = _allMembers.map((m) => m['contributionScore'] as int).reduce((a, b) => a + b) / totalMembers; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Métriques & Statistiques', style: TextStyle( fontWeight: FontWeight.bold, color: Color(0xFF6C5CE7), fontSize: 18, ), ), const SizedBox(height: 12), // Première ligne de métriques Row( children: [ Expanded( child: _buildMetricCard( 'Total Membres', totalMembers.toString(), '+$newThisMonth ce mois', Icons.people, const Color(0xFF6C5CE7), trend: newThisMonth > 0 ? 'up' : 'stable', ), ), const SizedBox(width: 8), Expanded( child: _buildMetricCard( 'Membres Actifs', activeMembers.toString(), '${((activeMembers / totalMembers) * 100).toStringAsFixed(1)}%', Icons.check_circle, const Color(0xFF00B894), trend: 'up', ), ), ], ), const SizedBox(height: 8), // Deuxième ligne de métriques Row( children: [ Expanded( child: _buildMetricCard( 'Score Moyen', avgContribution.toStringAsFixed(0), 'Contribution', Icons.trending_up, const Color(0xFF0984E3), trend: 'up', ), ), const SizedBox(width: 8), Expanded( child: _buildMetricCard( 'Nouveaux', newThisMonth.toString(), 'Ce mois', Icons.new_releases, const Color(0xFFF39C12), trend: newThisMonth > 0 ? 'up' : 'stable', ), ), ], ), ], ); } /// Carte de métrique avec design sophistiqué Widget _buildMetricCard( String title, String value, String subtitle, IconData icon, Color color, { String trend = 'stable', }) { 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, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: color, size: 20), ), const Spacer(), if (trend == 'up') const Icon(Icons.trending_up, color: Colors.green, size: 16) else if (trend == 'down') const Icon(Icons.trending_down, color: Colors.red, size: 16) else const Icon(Icons.trending_flat, color: Colors.grey, size: 16), ], ), const SizedBox(height: 12), Text( value, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 4), Text( title, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), const SizedBox(height: 2), Text( subtitle, style: const TextStyle( fontSize: 10, color: Color(0xFF9CA3AF), ), ), ], ), ); } /// Barre de recherche et filtres avancés Widget _buildSearchAndFilters() { return Column( children: [ // Barre de recherche principale Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( children: [ const Icon(Icons.search, color: Color(0xFF6B7280)), const SizedBox(width: 12), Expanded( child: TextField( controller: _searchController, decoration: const InputDecoration( hintText: 'Rechercher par nom, email, département...', border: InputBorder.none, hintStyle: TextStyle(color: Color(0xFF9CA3AF)), ), onChanged: (value) { setState(() { _searchQuery = value; }); }, ), ), if (_searchQuery.isNotEmpty) IconButton( onPressed: () { _searchController.clear(); setState(() { _searchQuery = ''; }); }, icon: const Icon(Icons.clear, color: Color(0xFF6B7280)), ), const SizedBox(width: 8), Container( height: 32, width: 1, color: const Color(0xFFE5E7EB), ), const SizedBox(width: 8), IconButton( onPressed: () { setState(() { _showAdvancedFilters = !_showAdvancedFilters; }); }, icon: Icon( _showAdvancedFilters ? Icons.filter_list_off : Icons.filter_list, color: _showAdvancedFilters ? const Color(0xFF6C5CE7) : const Color(0xFF6B7280), ), tooltip: 'Filtres avancés', ), IconButton( onPressed: () { setState(() { _isGridView = !_isGridView; }); }, icon: Icon( _isGridView ? Icons.view_list : Icons.grid_view, color: const Color(0xFF6B7280), ), tooltip: _isGridView ? 'Vue liste' : 'Vue grille', ), ], ), ), // Filtres avancés (conditionnels) if (_showAdvancedFilters) ...[ const SizedBox(height: 12), _buildAdvancedFilters(), ], // Barre de filtres rapides const SizedBox(height: 12), _buildQuickFilters(), ], ); } /// Filtres rapides horizontaux Widget _buildQuickFilters() { final filters = ['Tous', 'Actifs', 'Inactifs', 'Nouveaux', 'Suspendus']; return SizedBox( height: 40, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: filters.length, itemBuilder: (context, index) { final filter = filters[index]; final isSelected = _selectedFilter == filter; return Padding( padding: EdgeInsets.only( left: index == 0 ? 0 : 8, right: index == filters.length - 1 ? 0 : 0, ), child: FilterChip( label: Text(filter), selected: isSelected, onSelected: (selected) { setState(() { _selectedFilter = selected ? filter : 'Tous'; }); }, backgroundColor: Colors.white, selectedColor: const Color(0xFF6C5CE7).withOpacity(0.1), checkmarkColor: const Color(0xFF6C5CE7), labelStyle: TextStyle( color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFF6B7280), fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), side: BorderSide( color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFFE5E7EB), ), ), ); }, ), ); } /// Filtres avancés extensibles Widget _buildAdvancedFilters() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Filtres Avancés', style: TextStyle( fontWeight: FontWeight.w600, color: Color(0xFF374151), ), ), const SizedBox(height: 12), // Filtre par rôles Wrap( spacing: 8, runSpacing: 8, children: [ 'Membre Actif', 'Modérateur', 'Administrateur Org', 'Gestionnaire RH', 'Consultant', 'Membre Simple', ].map((role) { final isSelected = _selectedRoles.contains(role); return FilterChip( label: Text(role), selected: isSelected, onSelected: (selected) { setState(() { if (selected) { _selectedRoles.add(role); } else { _selectedRoles.remove(role); } }); }, backgroundColor: Colors.grey[50], selectedColor: const Color(0xFF6C5CE7).withOpacity(0.1), checkmarkColor: const Color(0xFF6C5CE7), labelStyle: TextStyle( color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFF6B7280), fontSize: 12, ), side: BorderSide( color: isSelected ? const Color(0xFF6C5CE7) : const Color(0xFFE5E7EB), ), ); }).toList(), ), const SizedBox(height: 12), // Actions de filtres Row( children: [ TextButton.icon( onPressed: () { setState(() { _selectedRoles.clear(); _selectedStatuses = ['Actif', 'Inactif', 'Suspendu', 'En attente']; _dateRange = null; }); }, icon: const Icon(Icons.clear_all, size: 16), label: const Text('Réinitialiser'), style: TextButton.styleFrom( foregroundColor: const Color(0xFF6B7280), ), ), const Spacer(), Text( '${_getFilteredMembers().length} résultat(s)', style: const TextStyle( color: Color(0xFF6B7280), fontSize: 12, ), ), ], ), ], ), ); } /// Onglets de catégories Widget _buildCategoryTabs() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: TabBar( controller: _tabController, tabs: const [ Tab(text: 'Tous', icon: Icon(Icons.people, size: 18)), Tab(text: 'Actifs', icon: Icon(Icons.check_circle, size: 18)), Tab(text: 'Équipes', icon: Icon(Icons.groups, size: 18)), Tab(text: 'Analytics', icon: Icon(Icons.analytics, size: 18)), ], labelColor: const Color(0xFF6C5CE7), unselectedLabelColor: const Color(0xFF6B7280), indicatorColor: const Color(0xFF6C5CE7), indicatorWeight: 3, labelStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), unselectedLabelStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.normal, ), ), ); } /// Affichage principal des membres (liste ou grille) Widget _buildMembersDisplay() { final filteredMembers = _getFilteredMembers(); if (filteredMembers.isEmpty) { return _buildEmptyState(); } return SizedBox( height: 600, // Hauteur fixe pour éviter les problèmes de layout child: TabBarView( controller: _tabController, children: [ // Onglet "Tous" _buildMembersList(filteredMembers), // Onglet "Actifs" _buildMembersList(filteredMembers.where((m) => m['status'] == 'Actif').toList()), // Onglet "Équipes" _buildTeamsView(filteredMembers), // Onglet "Analytics" _buildAnalyticsView(filteredMembers), ], ), ); } /// Liste des membres avec design sophistiqué Widget _buildMembersList(List> members) { if (_isGridView) { return _buildMembersGrid(members); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: members.length, itemBuilder: (context, index) { final member = members[index]; return _buildMemberCard(member); }, ); } /// Grille des membres Widget _buildMembersGrid(List> members) { return GridView.builder( padding: const EdgeInsets.symmetric(vertical: 8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.8, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: members.length, itemBuilder: (context, index) { final member = members[index]; return _buildMemberGridCard(member); }, ); } /// Carte de membre sophistiquée pour la vue liste Widget _buildMemberCard(Map member) { final joinDate = member['joinDate'] as DateTime; final lastActivity = member['lastActivity'] as DateTime; final contributionScore = member['contributionScore'] as int; return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: InkWell( onTap: () => _showMemberDetails(member), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // Avatar avec indicateur de statut Stack( children: [ CircleAvatar( radius: 24, backgroundColor: _getStatusColor(member['status']).withOpacity(0.1), child: Text( member['name'][0].toUpperCase(), style: TextStyle( color: _getStatusColor(member['status']), fontWeight: FontWeight.bold, fontSize: 18, ), ), ), Positioned( bottom: 0, right: 0, child: Container( width: 12, height: 12, decoration: BoxDecoration( color: _getStatusColor(member['status']), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), ), ), ], ), const SizedBox(width: 12), // Informations principales Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( member['name'], style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16, color: Color(0xFF1F2937), ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _getRoleColor(member['role']).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( member['role'], style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: _getRoleColor(member['role']), ), ), ), ], ), const SizedBox(height: 4), Text( member['email'], style: const TextStyle( color: Color(0xFF6B7280), fontSize: 14, ), ), const SizedBox(height: 4), Row( children: [ Icon( Icons.business, size: 12, color: Colors.grey[500], ), const SizedBox(width: 4), Text( member['department'], style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(width: 12), Icon( Icons.location_on, size: 12, color: Colors.grey[500], ), const SizedBox(width: 4), Expanded( child: Text( member['location'], style: TextStyle( fontSize: 12, color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Row( children: [ // Score de contribution Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getScoreColor(contributionScore).withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.star, size: 10, color: _getScoreColor(contributionScore), ), const SizedBox(width: 2), Text( contributionScore.toString(), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: _getScoreColor(contributionScore), ), ), ], ), ), const SizedBox(width: 8), Text( 'Rejoint ${_formatDate(joinDate)}', style: const TextStyle( fontSize: 10, color: Color(0xFF9CA3AF), ), ), const Spacer(), Text( 'Actif ${_formatRelativeTime(lastActivity)}', style: const TextStyle( fontSize: 10, color: Color(0xFF9CA3AF), ), ), ], ), ], ), ), // Actions PopupMenuButton( onSelected: (value) => _handleMemberAction(value, member), itemBuilder: (context) => [ const PopupMenuItem( value: 'view', child: Row( children: [ Icon(Icons.visibility, size: 16), SizedBox(width: 8), Text('Voir le profil'), ], ), ), const PopupMenuItem( value: 'edit', child: Row( children: [ Icon(Icons.edit, size: 16), SizedBox(width: 8), Text('Modifier'), ], ), ), const PopupMenuItem( value: 'message', child: Row( children: [ Icon(Icons.message, size: 16), SizedBox(width: 8), Text('Envoyer un message'), ], ), ), const PopupMenuItem( value: 'delete', child: Row( children: [ Icon(Icons.delete, size: 16, color: Colors.red), SizedBox(width: 8), Text('Supprimer', style: TextStyle(color: Colors.red)), ], ), ), ], child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.more_vert, size: 16, color: Color(0xFF6B7280), ), ), ), ], ), ), ), ); } // ═══════════════════════════════════════════════════════════════════════════ // MÉTHODES UTILITAIRES ET HELPERS // ═══════════════════════════════════════════════════════════════════════════ /// Filtre les membres selon les critères sélectionnés List> _getFilteredMembers() { return _allMembers.where((member) { // Filtre par recherche textuelle if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); final name = member['name'].toString().toLowerCase(); final email = member['email'].toString().toLowerCase(); final department = member['department'].toString().toLowerCase(); if (!name.contains(query) && !email.contains(query) && !department.contains(query)) { return false; } } // Filtre par statut rapide if (_selectedFilter != 'Tous') { switch (_selectedFilter) { case 'Actifs': if (member['status'] != 'Actif') return false; break; case 'Inactifs': if (member['status'] != 'Inactif') return false; break; case 'Nouveaux': final joinDate = member['joinDate'] as DateTime; final now = DateTime.now(); final isNewThisMonth = joinDate.year == now.year && joinDate.month == now.month; if (!isNewThisMonth) return false; break; case 'Suspendus': if (member['status'] != 'Suspendu') return false; break; } } // Filtre par rôles sélectionnés if (_selectedRoles.isNotEmpty && !_selectedRoles.contains(member['role'])) { return false; } return true; }).toList(); } /// Obtient la couleur selon le statut Color _getStatusColor(String status) { switch (status) { case 'Actif': return const Color(0xFF10B981); case 'Inactif': return const Color(0xFF6B7280); case 'Suspendu': return const Color(0xFFDC2626); case 'En attente': return const Color(0xFFF59E0B); default: return const Color(0xFF6B7280); } } /// Obtient la couleur selon le rôle Color _getRoleColor(String role) { switch (role) { case 'Super Administrateur': return const Color(0xFF7C3AED); case 'Administrateur Org': return const Color(0xFF6366F1); case 'Gestionnaire RH': return const Color(0xFF0EA5E9); case 'Modérateur': return const Color(0xFF059669); case 'Membre Actif': return const Color(0xFF6C5CE7); case 'Consultant': return const Color(0xFFF59E0B); case 'Membre Simple': return const Color(0xFF6B7280); default: return const Color(0xFF6B7280); } } /// Obtient la couleur selon le score de contribution Color _getScoreColor(int score) { if (score >= 90) return const Color(0xFF10B981); if (score >= 70) return const Color(0xFF0EA5E9); if (score >= 50) return const Color(0xFFF59E0B); return const Color(0xFFDC2626); } /// Formate une date String _formatDate(DateTime date) { final months = [ 'jan', 'fév', 'mar', 'avr', 'mai', 'jun', 'jul', 'aoû', 'sep', 'oct', 'nov', 'déc' ]; return '${date.day} ${months[date.month - 1]} ${date.year}'; } /// Formate un temps relatif String _formatRelativeTime(DateTime date) { final now = DateTime.now(); final difference = now.difference(date); if (difference.inDays > 30) { return 'il y a ${(difference.inDays / 30).floor()} mois'; } else if (difference.inDays > 0) { return 'il y a ${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}'; } else if (difference.inHours > 0) { return 'il y a ${difference.inHours}h'; } else { return 'à l\'instant'; } } /// Vérifie si l'utilisateur peut gérer les membres bool _canManageMembers(UserRole role) { return [ UserRole.superAdmin, UserRole.orgAdmin, UserRole.moderator, ].contains(role); } // ═══════════════════════════════════════════════════════════════════════════ // MÉTHODES D'ACTIONS ET DIALOGS // ═══════════════════════════════════════════════════════════════════════════ /// Affiche le dialog d'ajout de membre void _showAddMemberDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Ajouter un membre'), content: const Text('Fonctionnalité d\'ajout de membre à implémenter'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ), ); } /// Affiche les actions groupées void _showBulkActions() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Actions groupées à implémenter'), backgroundColor: Color(0xFF6C5CE7), ), ); } /// Exporte la liste des membres void _exportMembers() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Export des membres en cours...'), backgroundColor: Color(0xFF10B981), ), ); } /// Affiche les détails d'un membre void _showMemberDetails(Map member) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => _buildMemberDetailsSheet(member), ); } /// Gère les actions sur un membre void _handleMemberAction(String action, Map member) { switch (action) { case 'view': _showMemberDetails(member); break; case 'edit': _showEditMemberDialog(member); break; case 'message': _sendMessageToMember(member); break; case 'delete': _showDeleteMemberDialog(member); break; } } /// Dialog d'édition de membre void _showEditMemberDialog(Map member) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Modifier ${member['name']}'), content: const Text('Fonctionnalité de modification à implémenter'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Sauvegarder'), ), ], ), ); } /// Envoie un message à un membre void _sendMessageToMember(Map member) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Message à ${member['name']} à implémenter'), backgroundColor: const Color(0xFF0EA5E9), ), ); } /// Dialog de suppression de membre void _showDeleteMemberDialog(Map member) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Supprimer le membre'), content: Text('Êtes-vous sûr de vouloir supprimer ${member['name']} ?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${member['name']} supprimé'), backgroundColor: const Color(0xFFDC2626), ), ); }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('Supprimer'), ), ], ), ); } // ═══════════════════════════════════════════════════════════════════════════ // WIDGETS SPÉCIALISÉS ET VUES AVANCÉES // ═══════════════════════════════════════════════════════════════════════════ /// Carte de membre pour la vue grille Widget _buildMemberGridCard(Map member) { final contributionScore = member['contributionScore'] as int; return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: InkWell( onTap: () => _showMemberDetails(member), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // Avatar et statut Stack( children: [ CircleAvatar( radius: 30, backgroundColor: _getStatusColor(member['status']).withOpacity(0.1), child: Text( member['name'][0].toUpperCase(), style: TextStyle( color: _getStatusColor(member['status']), fontWeight: FontWeight.bold, fontSize: 20, ), ), ), Positioned( bottom: 0, right: 0, child: Container( width: 16, height: 16, decoration: BoxDecoration( color: _getStatusColor(member['status']), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), ), ), ], ), const SizedBox(height: 12), // Nom et rôle Text( member['name'], style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _getRoleColor(member['role']).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( member['role'], style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: _getRoleColor(member['role']), ), textAlign: TextAlign.center, ), ), const SizedBox(height: 8), // Score de contribution Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.star, size: 14, color: _getScoreColor(contributionScore), ), const SizedBox(width: 4), Text( contributionScore.toString(), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _getScoreColor(contributionScore), ), ), ], ), const Spacer(), // Actions rapides Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( onPressed: () => _showMemberDetails(member), icon: const Icon(Icons.visibility, size: 16), tooltip: 'Voir', ), IconButton( onPressed: () => _sendMessageToMember(member), icon: const Icon(Icons.message, size: 16), tooltip: 'Message', ), ], ), ], ), ), ), ); } /// État vide quand aucun membre ne correspond aux filtres Widget _buildEmptyState() { return SizedBox( height: 400, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFF6C5CE7).withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon( Icons.people_outline, size: 48, color: Color(0xFF6C5CE7), ), ), const SizedBox(height: 16), const Text( 'Aucun membre trouvé', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF374151), ), ), const SizedBox(height: 8), Text( _searchQuery.isNotEmpty ? 'Aucun membre ne correspond à votre recherche' : 'Aucun membre ne correspond aux filtres sélectionnés', style: const TextStyle( color: Color(0xFF6B7280), ), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { setState(() { _searchController.clear(); _searchQuery = ''; _selectedFilter = 'Tous'; _selectedRoles.clear(); }); }, icon: const Icon(Icons.refresh), label: const Text('Réinitialiser les filtres'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, ), ), ], ), ); } /// Vue des équipes (onglet Équipes) Widget _buildTeamsView(List> members) { final departments = >>{}; // Grouper par département for (final member in members) { final dept = member['department'] as String; departments[dept] = departments[dept] ?? []; departments[dept]!.add(member); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: departments.length, itemBuilder: (context, index) { final dept = departments.keys.elementAt(index); final deptMembers = departments[dept]!; return Card( margin: const EdgeInsets.only(bottom: 12), child: ExpansionTile( title: Text( dept, style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text('${deptMembers.length} membre(s)'), children: deptMembers.map((member) => ListTile( leading: CircleAvatar( backgroundColor: _getStatusColor(member['status']).withOpacity(0.1), child: Text( member['name'][0].toUpperCase(), style: TextStyle( color: _getStatusColor(member['status']), fontWeight: FontWeight.bold, ), ), ), title: Text(member['name']), subtitle: Text(member['role']), trailing: Text( member['contributionScore'].toString(), style: TextStyle( fontWeight: FontWeight.bold, color: _getScoreColor(member['contributionScore']), ), ), onTap: () => _showMemberDetails(member), )).toList(), ), ); }, ); } /// Vue analytics (onglet Analytics) Widget _buildAnalyticsView(List> members) { return SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ // Graphique de répartition par statut _buildStatusChart(members), const SizedBox(height: 16), // Graphique de répartition par rôle _buildRoleChart(members), const SizedBox(height: 16), // Top contributeurs _buildTopContributors(members), ], ), ); } /// Graphique de répartition par statut Widget _buildStatusChart(List> members) { final statusCounts = {}; for (final member in members) { final status = member['status'] as String; statusCounts[status] = (statusCounts[status] ?? 0) + 1; } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Répartition par Statut', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), ...statusCounts.entries.map((entry) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: _getStatusColor(entry.key), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Expanded(child: Text(entry.key)), Text( entry.value.toString(), style: const TextStyle(fontWeight: FontWeight.w600), ), ], ), )), ], ), ), ); } /// Graphique de répartition par rôle Widget _buildRoleChart(List> members) { final roleCounts = {}; for (final member in members) { final role = member['role'] as String; roleCounts[role] = (roleCounts[role] ?? 0) + 1; } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Répartition par Rôle', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), ...roleCounts.entries.map((entry) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: _getRoleColor(entry.key), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Expanded(child: Text(entry.key)), Text( entry.value.toString(), style: const TextStyle(fontWeight: FontWeight.w600), ), ], ), )), ], ), ), ); } /// Top contributeurs Widget _buildTopContributors(List> members) { final sortedMembers = List>.from(members); sortedMembers.sort((a, b) => (b['contributionScore'] as int).compareTo(a['contributionScore'] as int)); final topMembers = sortedMembers.take(5).toList(); return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Top Contributeurs', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), ...topMembers.asMap().entries.map((entry) { final index = entry.key; final member = entry.value; return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: index < 3 ? const Color(0xFFF59E0B) : const Color(0xFF6B7280), borderRadius: BorderRadius.circular(12), ), child: Center( child: Text( '${index + 1}', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 12), CircleAvatar( radius: 16, backgroundColor: _getStatusColor(member['status']).withOpacity(0.1), child: Text( member['name'][0].toUpperCase(), style: TextStyle( color: _getStatusColor(member['status']), fontWeight: FontWeight.bold, fontSize: 12, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( member['name'], style: const TextStyle(fontWeight: FontWeight.w500), ), Text( member['role'], style: const TextStyle( fontSize: 12, color: Color(0xFF6B7280), ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _getScoreColor(member['contributionScore']).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.star, size: 12, color: _getScoreColor(member['contributionScore']), ), const SizedBox(width: 2), Text( member['contributionScore'].toString(), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _getScoreColor(member['contributionScore']), ), ), ], ), ), ], ), ); }), ], ), ), ); } /// Sheet de détails d'un membre Widget _buildMemberDetailsSheet(Map member) { return DraggableScrollableSheet( initialChildSize: 0.7, minChildSize: 0.5, maxChildSize: 0.95, builder: (context, scrollController) { return Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ // Handle Container( margin: const EdgeInsets.symmetric(vertical: 8), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), // Header Padding( padding: const EdgeInsets.all(16), child: Row( children: [ CircleAvatar( radius: 30, backgroundColor: _getStatusColor(member['status']).withOpacity(0.1), child: Text( member['name'][0].toUpperCase(), style: TextStyle( color: _getStatusColor(member['status']), fontWeight: FontWeight.bold, fontSize: 24, ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( member['name'], style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), Text( member['role'], style: TextStyle( color: _getRoleColor(member['role']), fontWeight: FontWeight.w500, ), ), Container( margin: const EdgeInsets.only(top: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _getStatusColor(member['status']).withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( member['status'], style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: _getStatusColor(member['status']), ), ), ), ], ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), ), ], ), ), // Contenu détaillé Expanded( child: SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Informations de contact _buildDetailSection( 'Informations de Contact', [ _buildDetailItem(Icons.email, 'Email', member['email']), _buildDetailItem(Icons.phone, 'Téléphone', member['phone']), _buildDetailItem(Icons.location_on, 'Localisation', member['location']), ], ), // Informations professionnelles _buildDetailSection( 'Informations Professionnelles', [ _buildDetailItem(Icons.business, 'Département', member['department']), _buildDetailItem(Icons.admin_panel_settings, 'Permissions', '${member['permissions']} permissions'), _buildDetailItem(Icons.calendar_today, 'Date d\'adhésion', _formatDate(member['joinDate'])), _buildDetailItem(Icons.access_time, 'Dernière activité', _formatRelativeTime(member['lastActivity'])), ], ), // Statistiques d'activité _buildDetailSection( 'Statistiques d\'Activité', [ _buildDetailItem(Icons.star, 'Score de contribution', '${member['contributionScore']}/100'), _buildDetailItem(Icons.event, 'Événements participés', '${member['eventsAttended']} événements'), _buildDetailItem(Icons.work, 'Projets impliqués', '${member['projectsInvolved']} projets'), ], ), const SizedBox(height: 20), // Actions Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () { Navigator.of(context).pop(); _showEditMemberDialog(member); }, icon: const Icon(Icons.edit), label: const Text('Modifier'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C5CE7), foregroundColor: Colors.white, ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: () { Navigator.of(context).pop(); _sendMessageToMember(member); }, icon: const Icon(Icons.message), label: const Text('Message'), ), ), ], ), ], ), ), ), ], ), ); }, ); } /// Section de détails Widget _buildDetailSection(String title, List items) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF374151), ), ), const SizedBox(height: 12), ...items, const SizedBox(height: 20), ], ); } /// Item de détail Widget _buildDetailItem(IconData icon, String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Icon( icon, size: 20, color: const Color(0xFF6B7280), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 12, color: Color(0xFF6B7280), ), ), Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF374151), ), ), ], ), ), ], ), ); } }