import 'dart:async'; import 'package:flutter/material.dart'; import '../../../../shared/design_system/unionflow_design_v2.dart'; import '../../../../shared/design_system/components/uf_app_bar.dart'; import '../../../../core/constants/app_constants.dart'; /// Annuaire des Membres - Design UnionFlow class MembersPageWithDataAndPagination extends StatefulWidget { final List> members; final int totalCount; final int currentPage; final int totalPages; final Function(int page, String? recherche) onPageChanged; final VoidCallback onRefresh; final void Function(String? query)? onSearch; final VoidCallback? onAddMember; const MembersPageWithDataAndPagination({ super.key, required this.members, required this.totalCount, required this.currentPage, required this.totalPages, required this.onPageChanged, required this.onRefresh, this.onSearch, this.onAddMember, }); @override State createState() => _MembersPageWithDataAndPaginationState(); } class _MembersPageWithDataAndPaginationState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; String _filterStatus = 'Tous'; Timer? _searchDebounce; @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: UnionFlowColors.background, appBar: UFAppBar( title: 'Annuaire Membres', backgroundColor: UnionFlowColors.surface, foregroundColor: UnionFlowColors.textPrimary, actions: [ if (widget.onAddMember != null) IconButton( icon: const Icon(Icons.person_add_outlined), color: UnionFlowColors.unionGreen, onPressed: widget.onAddMember, tooltip: 'Ajouter un membre', ), const SizedBox(width: 8), ], ), body: Column( children: [ _buildHeader(), _buildSearchAndFilters(), Expanded(child: _buildMembersList()), if (widget.totalPages > 1) _buildPagination(), ], ), ); } Widget _buildHeader() { final activeCount = widget.members.where((m) => m['status'] == 'Actif').length; final pendingCount = widget.members.where((m) => m['status'] == 'En attente').length; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border( bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1), ), ), child: Row( children: [ Expanded(child: _buildStatBadge('Total', widget.totalCount.toString(), UnionFlowColors.unionGreen)), const SizedBox(width: 12), Expanded(child: _buildStatBadge('Actifs', activeCount.toString(), UnionFlowColors.success)), const SizedBox(width: 12), Expanded(child: _buildStatBadge('Attente', pendingCount.toString(), UnionFlowColors.warning)), ], ), ); } Widget _buildStatBadge(String label, String value, Color color) { return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.3), width: 1), ), child: Column( children: [ Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: color)), const SizedBox(height: 2), Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color)), ], ), ); } Widget _buildSearchAndFilters() { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border(bottom: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)), ), child: Column( children: [ TextField( controller: _searchController, onChanged: (v) { setState(() => _searchQuery = v); _searchDebounce?.cancel(); _searchDebounce = Timer(AppConstants.searchDebounce, () { widget.onSearch?.call(v.isEmpty ? null : v); }); }, style: const TextStyle(fontSize: 14, color: UnionFlowColors.textPrimary), decoration: InputDecoration( hintText: 'Rechercher un membre...', hintStyle: const TextStyle(fontSize: 13, color: UnionFlowColors.textTertiary), prefixIcon: const Icon(Icons.search, size: 20, color: UnionFlowColors.textSecondary), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 18, color: UnionFlowColors.textSecondary), onPressed: () { _searchDebounce?.cancel(); _searchController.clear(); setState(() => _searchQuery = ''); widget.onSearch?.call(null); }, ) : null, contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: UnionFlowColors.border.withOpacity(0.3)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: UnionFlowColors.unionGreen, width: 1.5), ), filled: true, fillColor: UnionFlowColors.surfaceVariant.withOpacity(0.3), ), ), const SizedBox(height: 12), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildFilterChip('Tous'), const SizedBox(width: 8), _buildFilterChip('Actif'), const SizedBox(width: 8), _buildFilterChip('Inactif'), const SizedBox(width: 8), _buildFilterChip('En attente'), ], ), ), ], ), ); } Widget _buildFilterChip(String label) { final isSelected = _filterStatus == label; return GestureDetector( onTap: () => setState(() => _filterStatus = label), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isSelected ? UnionFlowColors.unionGreen : UnionFlowColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: isSelected ? UnionFlowColors.unionGreen : UnionFlowColors.border, width: 1), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : UnionFlowColors.textSecondary, ), ), ), ); } Widget _buildMembersList() { final filtered = widget.members.where((m) { final matchesSearch = _searchQuery.isEmpty || m['name']!.toLowerCase().contains(_searchQuery.toLowerCase()) || (m['email']?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false); final matchesStatus = _filterStatus == 'Tous' || m['status'] == _filterStatus; return matchesSearch && matchesStatus; }).toList(); if (filtered.isEmpty) return _buildEmptyState(); return RefreshIndicator( onRefresh: () async => widget.onRefresh(), color: UnionFlowColors.unionGreen, child: ListView.separated( padding: const EdgeInsets.all(12), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 6), itemBuilder: (context, index) => _buildMemberCard(filtered[index]), ), ); } Widget _buildMemberCard(Map member) { return GestureDetector( onTap: () => _showMemberDetails(member), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), decoration: BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.circular(10), border: Border.all(color: UnionFlowColors.border.withOpacity(0.3), width: 1), ), child: Row( children: [ Container( width: 32, height: 32, decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle), alignment: Alignment.center, child: Text( member['initiales'] ?? '??', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( member['name'] ?? 'Inconnu', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: UnionFlowColors.textPrimary), ), const SizedBox(height: 2), Text( member['role'] ?? 'Membre', style: const TextStyle(fontSize: 12, color: UnionFlowColors.textSecondary), ), if (member['email'] != null) ...[ const SizedBox(height: 4), Row( children: [ const Icon(Icons.email_outlined, size: 12, color: UnionFlowColors.textTertiary), const SizedBox(width: 4), Text(member['email']!, style: const TextStyle(fontSize: 11, color: UnionFlowColors.textTertiary)), ], ), ], ], ), ), _buildStatusBadge(member['status']), ], ), ), ); } Widget _buildStatusBadge(String? status) { Color color; switch (status) { case 'Actif': color = UnionFlowColors.success; break; case 'Inactif': color = UnionFlowColors.error; break; case 'En attente': color = UnionFlowColors.warning; break; default: color = UnionFlowColors.textSecondary; } return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3), width: 1), ), child: Text(status ?? '?', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration(color: UnionFlowColors.unionGreenPale, shape: BoxShape.circle), child: const Icon(Icons.people_outline, size: 40, color: UnionFlowColors.unionGreen), ), const SizedBox(height: 12), const Text( 'Aucun membre trouvé', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), ), const SizedBox(height: 8), Text( _searchQuery.isEmpty ? 'Changez vos filtres' : 'Essayez une autre recherche', style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary), ), ], ), ); } Widget _buildPagination() { return Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: UnionFlowColors.surface, border: Border(top: BorderSide(color: UnionFlowColors.border.withOpacity(0.5), width: 1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( icon: const Icon(Icons.chevron_left, size: 24), color: widget.currentPage > 0 ? UnionFlowColors.unionGreen : UnionFlowColors.textTertiary, onPressed: widget.currentPage > 0 ? () => widget.onPageChanged( widget.currentPage - 1, _searchQuery.isEmpty ? null : _searchQuery, ) : null, ), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(gradient: UnionFlowColors.primaryGradient, borderRadius: BorderRadius.circular(20)), child: Text( 'Page ${widget.currentPage + 1} / ${widget.totalPages}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white), ), ), IconButton( icon: const Icon(Icons.chevron_right, size: 24), color: widget.currentPage < widget.totalPages - 1 ? UnionFlowColors.unionGreen : UnionFlowColors.textTertiary, onPressed: widget.currentPage < widget.totalPages - 1 ? () => widget.onPageChanged( widget.currentPage + 1, _searchQuery.isEmpty ? null : _searchQuery, ) : null, ), ], ), ); } void _showMemberDetails(Map member) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) => Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 56, height: 56, decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle), alignment: Alignment.center, child: Text( member['initiales'] ?? '??', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 22), ), ), const SizedBox(height: 10), Text( member['name'] ?? '', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), ), const SizedBox(height: 4), Text( member['role'] ?? '', style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary), ), const SizedBox(height: 12), _buildInfoRow(Icons.email_outlined, member['email'] ?? 'Non fourni'), _buildInfoRow(Icons.phone_outlined, member['phone'] ?? 'Non fourni'), _buildInfoRow(Icons.location_on_outlined, member['location'] ?? 'Non renseigné'), _buildInfoRow(Icons.work_outline, member['department'] ?? 'Aucun département'), const SizedBox(height: 12), ], ), ), ); } Widget _buildInfoRow(IconData icon, String text) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Icon(icon, size: 18, color: UnionFlowColors.unionGreen), const SizedBox(width: 12), Expanded(child: Text(text, style: const TextStyle(fontSize: 13, color: UnionFlowColors.textPrimary))), ], ), ); } }