Files
unionflow-server-api/unionflow-mobile-apps/lib/features/members/presentation/pages/members_page.dart
2025-09-20 03:56:11 +00:00

2040 lines
68 KiB
Dart

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/design_system/tokens/tokens.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<MembersPage> createState() => _MembersPageState();
}
class _MembersPageState extends State<MembersPage> with TickerProviderStateMixin {
// Controllers et état
final TextEditingController _searchController = TextEditingController();
late TabController _tabController;
// État de l'interface
String _searchQuery = '';
String _selectedFilter = 'Tous';
String _selectedSort = 'Nom';
bool _isGridView = false;
bool _showAdvancedFilters = false;
// Filtres avancés
List<String> _selectedRoles = [];
List<String> _selectedStatuses = ['Actif', 'Inactif', 'Suspendu', 'En attente'];
DateTimeRange? _dateRange;
// Données de démonstration enrichies
final List<Map<String, dynamic>> _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<AuthBloc, AuthState>(
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 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: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Gestion des Membres',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Interface complète de gestion des membres',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
],
),
),
if (canManageMembers) ...[
IconButton(
onPressed: () => _showBulkActions(),
icon: const Icon(Icons.checklist, color: Colors.white),
tooltip: 'Actions groupées',
),
IconButton(
onPressed: () => _exportMembers(),
icon: const Icon(Icons.download, color: Colors.white),
tooltip: 'Exporter',
),
IconButton(
onPressed: () => _showAddMemberDialog(),
icon: const Icon(Icons.person_add, color: Colors.white),
tooltip: 'Ajouter un membre',
),
],
],
),
const SizedBox(height: 12),
Row(
children: [
Icon(
Icons.access_time,
color: Colors.white.withOpacity(0.8),
size: 16,
),
const SizedBox(width: 4),
Text(
'Dernière mise à jour: ${_formatDateTime(DateTime.now())}',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
],
),
);
}
/// 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')
Icon(Icons.trending_up, color: Colors.green, size: 16)
else if (trend == 'down')
Icon(Icons.trending_down, color: Colors.red, size: 16)
else
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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<String, dynamic> member) {
final isActive = member['status'] == 'Actif';
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<String>(
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<Map<String, dynamic>> _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';
}
}
/// Formate une date et heure
String _formatDateTime(DateTime dateTime) {
return '${_formatDate(dateTime)} à ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
/// 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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 Container(
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<Map<String, dynamic>> members) {
final departments = <String, List<Map<String, dynamic>>>{};
// 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<Map<String, dynamic>> 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<Map<String, dynamic>> members) {
final statusCounts = <String, int>{};
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<Map<String, dynamic>> members) {
final roleCounts = <String, int>{};
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<Map<String, dynamic>> members) {
final sortedMembers = List<Map<String, dynamic>>.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<String, dynamic> 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<Widget> 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),
),
),
],
),
),
],
),
);
}
}