import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../../../shared/design_system/unionflow_design_v2.dart'; import '../../../../shared/design_system/components/uf_app_bar.dart'; import '../../../../core/constants/app_constants.dart'; import '../../../../features/organizations/domain/repositories/organization_repository.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; /// null = SUPER_ADMIN (vue globale, affiche l'organisation sur chaque carte) final String? organisationId; /// Callback déclenché quand l'admin active un membre en attente final void Function(String memberId)? onActivateMember; /// Callback déclenché quand l'admin réinitialise le mot de passe d'un membre final void Function(String memberId)? onResetPassword; /// Callback déclenché quand le superadmin affecte un membre à une organisation final void Function(String memberId, String organisationId)? onAffecterOrganisation; /// Callback pour les actions de cycle de vie adhésion (admin org) final void Function(String memberId, String action, String? motif)? onLifecycleAction; 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, this.organisationId, this.onActivateMember, this.onResetPassword, this.onAffecterOrganisation, this.onLifecycleAction, }); @override State createState() => _MembersPageWithDataAndPaginationState(); } class _MembersPageWithDataAndPaginationState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; String _filterStatus = 'Tous'; Timer? _searchDebounce; // Organisations pour le picker d'affectation (superadmin) List> _organisationsPicker = []; bool get _isSuperAdmin => widget.organisationId == null; @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(), ], ), ); } // ── Header ──────────────────────────────────────────────────────────────── Widget _buildHeader() { final pageMembers = widget.members; final activeCount = pageMembers.where((m) => m['status'] == 'Actif').length; final pendingCount = pageMembers.where((m) => m['status'] == 'En attente').length; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), 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, subtitle: 'global')), const SizedBox(width: 8), Expanded(child: _buildStatBadge('Actifs', activeCount.toString(), UnionFlowColors.success, subtitle: 'cette page')), const SizedBox(width: 8), Expanded(child: _buildStatBadge('Attente', pendingCount.toString(), UnionFlowColors.warning, subtitle: 'cette page')), ], ), ); } Widget _buildStatBadge(String label, String value, Color color, {String? subtitle}) { return Container( padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 6), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.25), width: 1), ), child: Column( children: [ Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: color)), Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: color)), if (subtitle != null) Text(subtitle, style: TextStyle(fontSize: 9, color: color.withOpacity(0.6))), ], ), ); } // ── Recherche + Filtres ──────────────────────────────────────────────────── Widget _buildSearchAndFilters() { return Container( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), 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: 'Nom, email, numéro 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: 11, horizontal: 14), 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: 10), 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: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isSelected ? UnionFlowColors.unionGreen : Colors.transparent, borderRadius: BorderRadius.circular(20), 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, ), ), ), ); } // ── Liste ───────────────────────────────────────────────────────────────── Widget _buildMembersList() { final filtered = widget.members.where((m) { final matchesSearch = _searchQuery.isEmpty || (m['name'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) || (m['email'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) || (m['numeroMembre'] as String? ?? '').toLowerCase().contains(_searchQuery.toLowerCase()); 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.fromLTRB(12, 12, 12, 12), itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 6), itemBuilder: (context, index) => _buildMemberCard(filtered[index]), ), ); } // ── Carte membre ────────────────────────────────────────────────────────── Widget _buildMemberCard(Map member) { final String? orgName = member['organisationNom'] as String?; final String? numero = member['numeroMembre'] as String?; final String status = member['status'] as String? ?? '?'; return GestureDetector( onTap: () => _showMemberDetails(member), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.circular(10), border: Border.all(color: UnionFlowColors.border.withOpacity(0.3), width: 1), ), child: Row( children: [ // Avatar Container( width: 38, height: 38, decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle), alignment: Alignment.center, child: Text( member['initiales'] as String? ?? '??', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14), ), ), const SizedBox(width: 12), // Infos principales Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nom + numéro Row( children: [ Expanded( child: Text( member['name'] as String? ?? 'Inconnu', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: UnionFlowColors.textPrimary), overflow: TextOverflow.ellipsis, ), ), if (numero != null && numero.isNotEmpty) ...[ const SizedBox(width: 6), Text( numero, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: UnionFlowColors.textTertiary), ), ], ], ), const SizedBox(height: 2), // Rôle Text( member['role'] as String? ?? 'Membre', style: const TextStyle(fontSize: 12, color: UnionFlowColors.textSecondary), ), // Organisation (SUPER_ADMIN uniquement) if (_isSuperAdmin && orgName != null && orgName.isNotEmpty) ...[ const SizedBox(height: 3), Row( children: [ const Icon(Icons.business_outlined, size: 11, color: UnionFlowColors.unionGreen), const SizedBox(width: 4), Expanded( child: Text( orgName, style: const TextStyle(fontSize: 11, color: UnionFlowColors.unionGreen, fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, ), ), ], ), ], // Email if (member['email'] != null) ...[ const SizedBox(height: 3), Row( children: [ const Icon(Icons.email_outlined, size: 11, color: UnionFlowColors.textTertiary), const SizedBox(width: 4), Expanded( child: Text( member['email'] as String, style: const TextStyle(fontSize: 11, color: UnionFlowColors.textTertiary), overflow: TextOverflow.ellipsis, ), ), ], ), ], ], ), ), const SizedBox(width: 8), _buildStatusBadge(status), ], ), ), ); } Widget _buildStatusBadge(String status) { final (Color color, IconData icon) = switch (status) { 'Actif' => (UnionFlowColors.success, Icons.check_circle_outline), 'Inactif' => (UnionFlowColors.error, Icons.cancel_outlined), 'En attente' => (UnionFlowColors.warning, Icons.schedule_outlined), 'Suspendu' => (const Color(0xFF9E9E9E), Icons.block_outlined), _ => (UnionFlowColors.textSecondary, Icons.help_outline), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 11, color: color), const SizedBox(width: 4), Text(status, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: color)), ], ), ); } // ── État vide ───────────────────────────────────────────────────────────── Widget _buildEmptyState() { final hasActiveFilters = _filterStatus != 'Tous' || _searchQuery.isNotEmpty; return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(18), decoration: const BoxDecoration(color: UnionFlowColors.unionGreenPale, shape: BoxShape.circle), child: Icon( hasActiveFilters ? Icons.filter_list_off : Icons.people_outline, size: 40, color: UnionFlowColors.unionGreen, ), ), const SizedBox(height: 16), Text( hasActiveFilters ? 'Aucun résultat' : 'Aucun membre', style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), ), const SizedBox(height: 8), Text( hasActiveFilters ? 'Modifiez la recherche ou le filtre de statut' : 'Aucun membre enregistré pour le moment', textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary), ), if (hasActiveFilters) ...[ const SizedBox(height: 16), GestureDetector( onTap: () { setState(() { _filterStatus = 'Tous'; _searchQuery = ''; _searchController.clear(); }); widget.onSearch?.call(null); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( border: Border.all(color: UnionFlowColors.unionGreen), borderRadius: BorderRadius.circular(20), ), child: const Text('Effacer les filtres', style: TextStyle(fontSize: 13, color: UnionFlowColors.unionGreen, fontWeight: FontWeight.w600)), ), ), ], ], ), ), ); } // ── Pagination ──────────────────────────────────────────────────────────── Widget _buildPagination() { return Container( padding: const EdgeInsets.symmetric(vertical: 10), 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, ), ], ), ); } // ── Sheet détail membre ─────────────────────────────────────────────────── void _showMemberDetails(Map member) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => DraggableScrollableSheet( initialChildSize: 0.55, minChildSize: 0.4, maxChildSize: 0.85, expand: false, builder: (context, scrollController) => Container( decoration: const BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ // Drag handle Container( margin: const EdgeInsets.only(top: 10), width: 36, height: 4, decoration: BoxDecoration(color: UnionFlowColors.border, borderRadius: BorderRadius.circular(2)), ), Expanded( child: SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête avatar + nom + statut Row( children: [ Container( width: 56, height: 56, decoration: const BoxDecoration(gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle), alignment: Alignment.center, child: Text( member['initiales'] as String? ?? '??', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 22), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( member['name'] as String? ?? '', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), ), const SizedBox(height: 3), Text( member['role'] as String? ?? 'Membre', style: const TextStyle(fontSize: 13, color: UnionFlowColors.textSecondary), ), if (member['numeroMembre'] != null) ...[ const SizedBox(height: 3), Text( member['numeroMembre'] as String, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: UnionFlowColors.textTertiary), ), ], ], ), ), _buildStatusBadge(member['status'] as String? ?? '?'), ], ), const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 12), // Infos contact _buildSectionTitle('Contact'), _buildDetailRow(Icons.email_outlined, 'Email', member['email'] as String? ?? '—'), if ((member['phone'] as String? ?? '').isNotEmpty) _buildDetailRow(Icons.phone_outlined, 'Téléphone', member['phone'] as String), const SizedBox(height: 12), // Infos organisation if (member['organisationNom'] != null || member['organisationId'] != null) ...[ _buildSectionTitle('Organisation'), if (member['organisationNom'] != null) _buildDetailRow(Icons.business_outlined, 'Organisation', member['organisationNom'] as String), if (member['dateAdhesion'] != null) _buildDetailRow(Icons.calendar_today_outlined, 'Adhésion', _formatDate(member['dateAdhesion'])), const SizedBox(height: 12), ], // Infos pro if ((member['department'] as String? ?? '').isNotEmpty) ...[ _buildSectionTitle('Profil'), _buildDetailRow(Icons.work_outline, 'Profession', member['department'] as String), if ((member['nationalite'] as String? ?? '').isNotEmpty) _buildDetailRow(Icons.flag_outlined, 'Nationalité', member['nationalite'] as String), if ((member['location'] as String? ?? ', ').trim() != ',') _buildDetailRow(Icons.location_on_outlined, 'Localisation', member['location'] as String), ], // Bouton d'activation (ADMIN_ORGANISATION uniquement, membre en attente) if (member['status'] == 'En attente' && widget.onActivateMember != null) ...[ const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); widget.onActivateMember!(member['id'] as String); }, icon: const Icon(Icons.check_circle_outline, size: 18), label: const Text('Activer le membre', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: UnionFlowColors.success, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ), ], // Bouton reset mot de passe (tous membres avec compte Keycloak) if (widget.onResetPassword != null) ...[ const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 16), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () { Navigator.pop(context); widget.onResetPassword!(member['id'] as String); }, icon: const Icon(Icons.lock_reset_outlined, size: 18), label: const Text('Réinitialiser le mot de passe', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: OutlinedButton.styleFrom( foregroundColor: UnionFlowColors.warning, side: const BorderSide(color: UnionFlowColors.warning), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ), ], // Bouton affectation organisation (superadmin, membre sans organisation) if (_isSuperAdmin && widget.onAffecterOrganisation != null && (member['organisationId'] == null || (member['organisationId'] as String).isEmpty)) ...[ const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 16), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () => _showAffecterOrganisationDialog(context, member), icon: const Icon(Icons.business_outlined, size: 18), label: const Text('Affecter à une organisation', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: OutlinedButton.styleFrom( foregroundColor: UnionFlowColors.unionGreen, side: const BorderSide(color: UnionFlowColors.unionGreen), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ), ], // ── Boutons cycle de vie adhésion (ADMIN_ORGANISATION) ── if (!_isSuperAdmin && widget.onLifecycleAction != null) ...[ const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 8), _buildLifecycleActionsSection(context, member), ], ], ), ), ), ], ), ), ), ); } Widget _buildLifecycleActionsSection(BuildContext context, Map member) { final memberId = member['id'] as String? ?? ''; final statut = member['statutMembre'] as String? ?? member['status'] as String? ?? ''; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.only(bottom: 8), child: Text('Actions adhésion', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.grey)), ), // Activer (INVITE ou EN_ATTENTE_VALIDATION) if (statut == 'INVITE' || statut == 'EN_ATTENTE_VALIDATION' || statut == 'En attente') _lifecycleButton( context: context, label: 'Activer l\'adhésion', icon: Icons.check_circle_outline, color: Colors.green, onPressed: () { Navigator.pop(context); widget.onLifecycleAction!(memberId, 'activer', null); }, ), // Suspendre (ACTIF) if (statut == 'ACTIF' || statut == 'Actif') _lifecycleButton( context: context, label: 'Suspendre l\'adhésion', icon: Icons.pause_circle_outline, color: Colors.orange, outlined: true, onPressed: () => _showMotifDialog( context, 'Suspendre l\'adhésion', onConfirm: (motif) => widget.onLifecycleAction!(memberId, 'suspendre', motif), ), ), // Réactiver (SUSPENDU) if (statut == 'SUSPENDU' || statut == 'Suspendu') _lifecycleButton( context: context, label: 'Réactiver l\'adhésion', icon: Icons.play_circle_outline, color: Colors.blue, onPressed: () { Navigator.pop(context); widget.onLifecycleAction!(memberId, 'activer', null); }, ), // Radier (tout statut actif) if (statut != 'RADIE' && statut != 'ARCHIVE' && statut.isNotEmpty) _lifecycleButton( context: context, label: 'Radier de l\'organisation', icon: Icons.block_outlined, color: Colors.red, outlined: true, onPressed: () => _showMotifDialog( context, 'Radier le membre', onConfirm: (motif) => widget.onLifecycleAction!(memberId, 'radier', motif), ), ), ], ); } Widget _lifecycleButton({ required BuildContext context, required String label, required IconData icon, required Color color, bool outlined = false, required VoidCallback onPressed, }) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: SizedBox( width: double.infinity, child: outlined ? OutlinedButton.icon( onPressed: onPressed, icon: Icon(icon, size: 18), label: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: OutlinedButton.styleFrom( foregroundColor: color, side: BorderSide(color: color), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ) : ElevatedButton.icon( onPressed: onPressed, icon: Icon(icon, size: 18), label: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ), ); } void _showMotifDialog( BuildContext context, String titre, { required void Function(String? motif) onConfirm, }) { final motifCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(titre), content: TextField( controller: motifCtrl, decoration: const InputDecoration( labelText: 'Motif (optionnel)', border: OutlineInputBorder(), ), maxLines: 3, ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { Navigator.pop(ctx); // ferme dialog motif Navigator.pop(context); // ferme bottom sheet onConfirm(motifCtrl.text.isNotEmpty ? motifCtrl.text : null); }, child: const Text('Confirmer'), ), ], ), ); } Future _showAffecterOrganisationDialog( BuildContext ctx, Map member, ) async { // Charger les organisations si pas encore fait if (_organisationsPicker.isEmpty) { try { final repo = GetIt.instance(); final orgs = await repo.getOrganizations(page: 0, size: 100); if (mounted) { setState(() { _organisationsPicker = orgs .where((o) => o.id != null && o.id!.isNotEmpty) .map((o) => {'id': o.id!, 'nom': o.nomAffichage}) .toList(); }); } } catch (_) {} } if (!mounted) return; String? selectedOrgId; await showDialog( context: ctx, builder: (dialogCtx) => StatefulBuilder( builder: (dialogCtx, setDialogState) => AlertDialog( title: const Text('Affecter à une organisation'), content: _organisationsPicker.isEmpty ? const Text('Aucune organisation disponible.') : DropdownButtonFormField( decoration: const InputDecoration(labelText: 'Organisation'), items: _organisationsPicker .map((o) => DropdownMenuItem( value: o['id'], child: Text(o['nom']!), )) .toList(), onChanged: (v) => setDialogState(() => selectedOrgId = v), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogCtx), child: const Text('Annuler'), ), ElevatedButton( onPressed: selectedOrgId == null ? null : () { Navigator.pop(dialogCtx); Navigator.pop(ctx); // ferme le bottom sheet widget.onAffecterOrganisation!( member['id'] as String, selectedOrgId!, ); }, child: const Text('Confirmer'), ), ], ), ), ); } Widget _buildSectionTitle(String title) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( title.toUpperCase(), style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: UnionFlowColors.textTertiary, letterSpacing: 0.8), ), ); } Widget _buildDetailRow(IconData icon, String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 16, color: UnionFlowColors.unionGreen), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 11, color: UnionFlowColors.textTertiary)), const SizedBox(height: 1), Text(value, style: const TextStyle(fontSize: 13, color: UnionFlowColors.textPrimary, fontWeight: FontWeight.w500)), ], ), ), ], ), ); } String _formatDate(dynamic date) { if (date == null) return '—'; if (date is DateTime) return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; return date.toString(); } }