1963 lines
65 KiB
Dart
1963 lines
65 KiB
Dart
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<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';
|
|
|
|
bool _isGridView = false;
|
|
bool _showAdvancedFilters = false;
|
|
|
|
// Filtres avancés
|
|
final 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 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<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 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';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// 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 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<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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|