import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../bloc/organizations_bloc.dart'; import '../../bloc/organizations_event.dart'; import '../../bloc/organizations_state.dart'; import '../../data/models/organization_model.dart'; import '../widgets/organization_card.dart'; import '../widgets/create_organization_dialog.dart'; import '../widgets/edit_organization_dialog.dart'; import 'organization_detail_page.dart'; import 'org_types_page.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/design_system/components/animated_fade_in.dart'; import '../../../../shared/design_system/components/animated_slide_in.dart'; import '../../../../shared/design_system/components/african_pattern_background.dart'; import '../../../../shared/design_system/components/uf_app_bar.dart'; import '../../../../features/authentication/presentation/bloc/auth_bloc.dart'; import '../../../../features/authentication/data/models/user_role.dart'; import '../../bloc/org_switcher_bloc.dart'; /// Page de gestion des organisations - Interface sophistiquée et exhaustive class OrganizationsPage extends StatefulWidget { const OrganizationsPage({super.key}); @override State createState() => _OrganizationsPageState(); } class _OrganizationsPageState extends State with TickerProviderStateMixin { final TextEditingController _searchController = TextEditingController(); TabController? _tabController; final ScrollController _scrollController = ScrollController(); List _availableTypes = []; // ── Nouvelles fonctionnalités (parité Annuaire Membres) ── bool _isGridView = false; String _filterStatut = 'Tous'; static const _statutFilters = ['Tous', 'Active', 'En création', 'Inactive', 'Suspendue', 'Dissoute']; static const _statutFilterMapping = { 'Active': 'ACTIVE', 'En création': 'EN_CREATION', 'Inactive': 'INACTIVE', 'Suspendue': 'SUSPENDUE', 'Dissoute': 'DISSOUTE', }; /// Cache de la dernière liste connue. /// Évite de perdre l'affichage quand le bloc passe dans un état non-liste /// (OrganizationLoaded, OrganizationCreated, etc.) après navigation vers /// la page de détail ou une action CRUD. OrganizationsLoaded? _cachedListState; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } @override void dispose() { _tabController?.dispose(); _searchController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) { context.read().add(const LoadMoreOrganizations()); } } /// Titre dynamique de l'AppBar selon le rôle et le nombre d'organisations. /// /// SuperAdmin → "Gestion des Organisations" /// OrgAdmin 1 org → "Mon Organisation" /// OrgAdmin 2+ orgs → "Mes Organisations" String _appBarTitle(BuildContext context) { final authState = context.read().state; if (authState is! AuthAuthenticated) return 'Organisations'; if (authState.effectiveRole == UserRole.superAdmin) { return 'Gestion des Organisations'; } try { final switcherState = context.read().state; if (switcherState is OrgSwitcherLoaded) { return switcherState.organisations.length > 1 ? 'Mes Organisations' : 'Mon Organisation'; } } catch (_) {} return 'Mon Organisation'; } List _calculateAvailableTypes(List organizations) { if (organizations.isEmpty) return [null]; final types = organizations.map((org) => org.typeOrganisation).toSet().toList()..sort((a, b) => a.compareTo(b)); return [null, ...types]; } void _updateTabController(List newTypes) { if (_availableTypes.length != newTypes.length || !_availableTypes.every((t) => newTypes.contains(t))) { _availableTypes = newTypes; _tabController?.dispose(); _tabController = TabController(length: _availableTypes.length, vsync: this); } } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { if (state is OrganizationsError) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(state.message), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 4), action: SnackBarAction( label: 'Réessayer', textColor: AppColors.onError, onPressed: () => context.read().add(const LoadOrganizations(refresh: true)), ), )); } else if (state is OrganizationCreated) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Organisation créée avec succès'), backgroundColor: AppColors.success, duration: Duration(seconds: 2), )); } else if (state is OrganizationUpdated) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Organisation mise à jour avec succès'), backgroundColor: AppColors.success, duration: Duration(seconds: 2), )); } else if (state is OrganizationDeleted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Organisation supprimée avec succès'), backgroundColor: AppColors.success, duration: Duration(seconds: 2), )); } }, builder: (context, state) { // Détermine la liste d'organisations courante (avec fallback cache // pour ne pas perdre les onglets entre navigations) OrganizationsLoaded? loadedState; if (state is OrganizationsLoaded) { loadedState = state; } else if (_cachedListState != null) { loadedState = _cachedListState; } final availableTypes = loadedState != null ? _calculateAvailableTypes(loadedState.organizations) : []; _updateTabController(availableTypes); return AfricanPatternBackground( child: Scaffold( backgroundColor: Colors.transparent, appBar: UFAppBar( title: _appBarTitle(context), moduleGradient: ModuleColors.organisationsGradient, elevation: 0, actions: [ IconButton( icon: Icon(_isGridView ? Icons.view_list_rounded : Icons.grid_view_rounded), onPressed: () => setState(() => _isGridView = !_isGridView), tooltip: _isGridView ? 'Vue liste' : 'Vue grille', ), IconButton( icon: const Icon(Icons.category_outlined), onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const OrgTypesPage())), tooltip: 'Types d\'organisations', ), IconButton( icon: const Icon(Icons.refresh), onPressed: () => context.read().add(const RefreshOrganizations()), tooltip: 'Rafraîchir', ), ], bottom: (_tabController != null && availableTypes.isNotEmpty) ? TabBar( controller: _tabController!, isScrollable: true, labelColor: Colors.white, unselectedLabelColor: Colors.white70, indicatorColor: Colors.white, indicatorSize: TabBarIndicatorSize.label, labelStyle: AppTypography.actionText .copyWith(fontSize: 10, fontWeight: FontWeight.bold), onTap: (i) { final selectedType = availableTypes[i]; if (selectedType != null) { context .read() .add(FilterOrganizationsByType(selectedType)); } else { context .read() .add(const ClearOrganizationsFilters()); } }, tabs: availableTypes .map((type) => Tab( child: Text((type ?? 'Toutes').toUpperCase()))) .toList(), ) : null, ), body: SafeArea(child: _buildBody(context, state)), floatingActionButton: _buildActionButton(context, state), ), ); }, ); } Widget _buildBody(BuildContext context, OrganizationsState state) { // Mémoriser le dernier état liste dès qu'on l'obtient if (state is OrganizationsLoaded) _cachedListState = state; if (state is OrganizationsInitial || state is OrganizationsLoading) { // Données déjà en cache (ex. retour depuis détail) → afficher sans loader if (_cachedListState != null) return _buildLoadedBody(context, _cachedListState!); return _buildLoadingState(context); } if (state is OrganizationsLoaded) { return _buildLoadedBody(context, state); } if (state is OrganizationsLoadingMore) { return SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(SpacingTokens.md), physics: const AlwaysScrollableScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLoadingMorePlaceholder(context, state.currentOrganizations), Padding( padding: const EdgeInsets.all(SpacingTokens.md), child: Center(child: CircularProgressIndicator(color: ModuleColors.organisations)), ), const SizedBox(height: 80), ], ), ); } if (state is OrganizationsError) return _buildErrorState(context, state); // État non-liste (OrganizationLoaded, OrganizationCreated, etc.) : // réutiliser la liste en cache plutôt que d'afficher un loader indéfini. if (_cachedListState != null) return _buildLoadedBody(context, _cachedListState!); return _buildLoadingState(context); } /// Contenu principal de la liste — extrait pour être réutilisable via le cache. /// Note : les onglets de catégories (TabBar) sont désormais dans /// UFAppBar.bottom (pattern Adhésions). _updateTabController est appelé /// au niveau supérieur dans le builder du BlocConsumer. Widget _buildLoadedBody(BuildContext context, OrganizationsLoaded state) { return RefreshIndicator( color: ModuleColors.organisations, onRefresh: () async => context.read().add(const RefreshOrganizations()), child: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(SpacingTokens.md), physics: const AlwaysScrollableScrollPhysics(), child: AnimatedFadeIn( duration: const Duration(milliseconds: 400), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AnimatedSlideIn( duration: const Duration(milliseconds: 500), curve: Curves.easeOut, child: _buildKpiHeader(context, state), ), const SizedBox(height: SpacingTokens.md), AnimatedSlideIn( duration: const Duration(milliseconds: 600), curve: Curves.easeOut, child: _buildSearchBar(context, state), ), const SizedBox(height: SpacingTokens.sm), _buildStatutChips(context), const SizedBox(height: SpacingTokens.md), AnimatedSlideIn( duration: const Duration(milliseconds: 800), curve: Curves.easeOut, child: _isGridView ? _buildOrganizationsGrid(context, state) : _buildOrganizationsList(context, state), ), const SizedBox(height: 80), ], ), ), ), ); } // ─── KPI Header ─────────────────────────────────────────────────────────── Widget _buildKpiHeader(BuildContext context, OrganizationsLoaded state) { final orgs = state.organizations; final total = orgs.length; final active = orgs.where((o) => o.statut == StatutOrganization.active).length; final suspended = orgs.where((o) => o.statut == StatutOrganization.suspendue).length; final totalMembers = orgs.fold(0, (sum, o) => sum + o.nombreMembres); final activePercent = total > 0 ? active / total : 0.0; return Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(RadiusTokens.lg), border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1), boxShadow: const [BoxShadow(color: AppColors.shadow, blurRadius: 8, offset: Offset(0, 2))], ), child: Row( children: [ _kpiTile(context, 'Total', total.toString(), ModuleColors.organisations, Icons.business_outlined), _kpiSep(context), _kpiTileProgress(context, 'Actives', '${(activePercent * 100).toStringAsFixed(0)}%', AppColors.success, activePercent), _kpiSep(context), _kpiTile(context, 'Membres', totalMembers.toString(), ModuleColors.organisations, Icons.people_outline), _kpiSep(context), _kpiTile(context, 'Suspendues', suspended.toString(), AppColors.warning, Icons.pause_circle_outline), ], ), ); } Widget _kpiTile(BuildContext context, String label, String value, Color color, IconData icon) { return Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: color), const SizedBox(height: 1), Text(value, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: color)), Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color.withOpacity(0.75)), textAlign: TextAlign.center), ], ), ); } Widget _kpiTileProgress(BuildContext context, String label, String value, Color color, double progress) { return Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(value, style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800, color: color)), const SizedBox(height: 3), ClipRRect( borderRadius: BorderRadius.circular(2), child: LinearProgressIndicator( value: progress, backgroundColor: color.withOpacity(0.15), valueColor: AlwaysStoppedAnimation(color), minHeight: 4, ), ), const SizedBox(height: 2), Text(label, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w600, color: color.withOpacity(0.75)), textAlign: TextAlign.center), ], ), ); } Widget _kpiSep(BuildContext context) => Container(width: 1, height: 42, color: Theme.of(context).colorScheme.outlineVariant); // ─── Barre de recherche ─────────────────────────────────────────────────── Widget _buildSearchBar(BuildContext context, OrganizationsLoaded state) { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(RadiusTokens.lg), border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1), boxShadow: const [BoxShadow(color: AppColors.shadow, blurRadius: 8, offset: Offset(0, 2))], ), child: TextField( controller: _searchController, onChanged: (v) => context.read().add(SearchOrganizations(v)), style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface), decoration: InputDecoration( hintText: 'Rechercher par nom, type, localisation...', hintStyle: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14), prefixIcon: const Icon(Icons.search, color: ModuleColors.organisations), suffixIcon: _searchController.text.isNotEmpty ? IconButton( onPressed: () { _searchController.clear(); context.read().add(const SearchOrganizations('')); }, icon: Icon(Icons.clear, color: Theme.of(context).colorScheme.onSurfaceVariant), ) : null, border: OutlineInputBorder(borderRadius: BorderRadius.circular(RadiusTokens.lg), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(RadiusTokens.lg), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(RadiusTokens.lg), borderSide: const BorderSide(color: ModuleColors.organisations, width: 1.5)), filled: true, fillColor: Theme.of(context).colorScheme.surface, contentPadding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.sm), ), ), ); } // ─── Chips de filtre par statut ───────────────────────────────────────── Widget _buildStatutChips(BuildContext context) { return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _statutFilters.map((label) { final isSelected = _filterStatut == label; return Padding( padding: const EdgeInsets.only(right: 8), child: GestureDetector( onTap: () => setState(() => _filterStatut = label), child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isSelected ? ModuleColors.organisations : Colors.transparent, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? ModuleColors.organisations : Theme.of(context).colorScheme.outline, width: 1, ), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: isSelected ? AppColors.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ), ); }).toList(), ), ); } /// Filtre les organisations par statut (chips) en plus des filtres BLoC (type + recherche) List _applyStatutFilter(List orgs) { if (_filterStatut == 'Tous') return orgs; final mappedStatut = _statutFilterMapping[_filterStatut]; if (mappedStatut == null) return orgs; return orgs.where((o) => o.statut.name.toUpperCase() == mappedStatut).toList(); } // ─── Liste des organisations (avec swipe) ───────────────────────────────── Widget _buildOrganizationsList(BuildContext context, OrganizationsLoaded state) { final organizations = _applyStatutFilter(state.filteredOrganizations); if (organizations.isEmpty) return _buildEmptyState(context); final authState = context.read().state; final canManageOrgs = authState is AuthAuthenticated && (authState.effectiveRole == UserRole.superAdmin || authState.effectiveRole == UserRole.orgAdmin); return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: organizations.length, separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.sm), itemBuilder: (context, i) { final org = organizations[i]; return AnimatedFadeIn( duration: Duration(milliseconds: 300 + (i * 50)), child: _buildSwipeableCard(context, org, canManageOrgs), ); }, ); } // ─── Vue grille ─────────────────────────────────────────────────────────── Widget _buildOrganizationsGrid(BuildContext context, OrganizationsLoaded state) { final organizations = _applyStatutFilter(state.filteredOrganizations); if (organizations.isEmpty) return _buildEmptyState(context); return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: 0.85, ), itemCount: organizations.length, itemBuilder: (context, i) { final org = organizations[i]; return _buildGridCard(context, org); }, ); } Widget _buildGridCard(BuildContext context, OrganizationModel org) { final scheme = Theme.of(context).colorScheme; final statutColor = Color(int.parse(org.statut.color.substring(1), radix: 16) + 0xFF000000); return GestureDetector( onTap: () => _showOrganizationDetails(org), child: Container( decoration: BoxDecoration( color: scheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: scheme.outline.withOpacity(0.3)), boxShadow: [BoxShadow(color: AppColors.shadow, blurRadius: 4, offset: const Offset(0, 2))], ), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Icône + statut Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: ModuleColors.organisations.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.business_outlined, size: 20, color: ModuleColors.organisations), ), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: statutColor.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Text( org.statut.displayName, style: TextStyle(fontSize: 9, fontWeight: FontWeight.w700, color: statutColor), ), ), ], ), const SizedBox(height: 10), // Nom Text( org.nom, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: scheme.onSurface), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (org.nomCourt?.isNotEmpty == true) Text(org.nomCourt!, style: TextStyle(fontSize: 10, color: scheme.onSurfaceVariant)), const Spacer(), // Type Text( org.typeOrganisation, style: TextStyle(fontSize: 10, color: ModuleColors.organisations, fontWeight: FontWeight.w600), ), const SizedBox(height: 4), // Localisation + membres Row( children: [ if (org.ville?.isNotEmpty == true) ...[ Icon(Icons.location_on_outlined, size: 11, color: scheme.onSurfaceVariant), const SizedBox(width: 2), Expanded( child: Text( org.ville!, style: TextStyle(fontSize: 10, color: scheme.onSurfaceVariant), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], Icon(Icons.people_outline, size: 11, color: scheme.onSurfaceVariant), const SizedBox(width: 2), Text( '${org.nombreMembres}', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: scheme.onSurface), ), ], ), ], ), ), ); } // ─── Swipe actions ──────────────────────────────────────────────────────── Widget _buildSwipeableCard(BuildContext context, OrganizationModel org, bool canManage) { if (!canManage) { return OrganizationCard( organization: org, onTap: () => _showOrganizationDetails(org), showActions: false, ); } return Dismissible( key: Key('org_swipe_${org.id}'), direction: DismissDirection.horizontal, confirmDismiss: (dir) async { if (dir == DismissDirection.startToEnd) { _showEditOrganizationDialog(org); } else if (dir == DismissDirection.endToStart) { _confirmDeleteOrganization(org); } return false; }, background: Container( margin: const EdgeInsets.symmetric(vertical: 1), decoration: BoxDecoration( color: ModuleColors.organisations.withOpacity(0.13), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 20), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.edit_outlined, color: ModuleColors.organisations, size: 22), SizedBox(height: 4), Text('Modifier', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: ModuleColors.organisations)), ], ), ), secondaryBackground: Container( margin: const EdgeInsets.symmetric(vertical: 1), decoration: BoxDecoration( color: AppColors.error.withOpacity(0.13), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.centerRight, padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.delete_outline, color: AppColors.error, size: 22), const SizedBox(height: 4), Text('Supprimer', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.error)), ], ), ), child: OrganizationCard( organization: org, onTap: () => _showOrganizationDetails(org), onEdit: () => _showEditOrganizationDialog(org), onDelete: () => _confirmDeleteOrganization(org), showActions: canManage, ), ); } Widget _buildLoadingMorePlaceholder(BuildContext context, List orgs) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: orgs.length, separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.sm), itemBuilder: (context, i) => OrganizationCard(organization: orgs[i], onTap: () => _showOrganizationDetails(orgs[i]), showActions: false), ); } // ─── FAB ────────────────────────────────────────────────────────────────── Widget? _buildActionButton(BuildContext context, OrganizationsState state) { // Afficher le FAB si on a des données (état direct ou cache) final hasData = state is OrganizationsLoaded || state is OrganizationsLoadingMore || _cachedListState != null; if (!hasData) return null; final authState = context.read().state; if (authState is! AuthAuthenticated || authState.effectiveRole != UserRole.superAdmin) return null; return FloatingActionButton( onPressed: _showCreateOrganizationDialog, backgroundColor: ModuleColors.organisations, foregroundColor: AppColors.onPrimary, elevation: 6, tooltip: 'Nouvelle organisation', child: const Icon(Icons.add), ); } // ─── Empty state ────────────────────────────────────────────────────────── Widget _buildEmptyState(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(SpacingTokens.xl), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration(color: ModuleColors.organisations.withOpacity(0.08), shape: BoxShape.circle), child: const Icon(Icons.business_outlined, size: 56, color: ModuleColors.organisations), ), const SizedBox(height: SpacingTokens.lg), Text( 'Aucune organisation trouvée', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface), ), const SizedBox(height: SpacingTokens.xs), Text( 'Modifiez vos critères de recherche\nou créez une nouvelle organisation.', style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.5), textAlign: TextAlign.center, ), const SizedBox(height: SpacingTokens.lg), FilledButton.icon( onPressed: () { context.read().add(const ClearOrganizationsFilters()); _searchController.clear(); }, icon: const Icon(Icons.filter_list_off, size: 18), label: const Text('Réinitialiser les filtres'), style: FilledButton.styleFrom(backgroundColor: ModuleColors.organisations), ), ], ), ), ); } // ─── Loading state ──────────────────────────────────────────────────────── Widget _buildLoadingState(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: ModuleColors.organisations, strokeWidth: 3), const SizedBox(height: SpacingTokens.md), Text( 'Chargement des organisations...', style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), ); } // ─── Error state ────────────────────────────────────────────────────────── Widget _buildErrorState(BuildContext context, OrganizationsError state) { final cs = Theme.of(context).colorScheme; return Center( child: Container( margin: const EdgeInsets.all(SpacingTokens.xl), padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( color: cs.errorContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(RadiusTokens.lg), border: Border.all(color: cs.error.withOpacity(0.3), width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 56, color: cs.error), const SizedBox(height: SpacingTokens.md), Text( state.message, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface), textAlign: TextAlign.center, ), if (state.details != null) ...[ const SizedBox(height: SpacingTokens.xs), Text(state.details!, style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), textAlign: TextAlign.center), ], const SizedBox(height: SpacingTokens.lg), FilledButton.icon( onPressed: () => context.read().add(const LoadOrganizations(refresh: true)), icon: const Icon(Icons.refresh, size: 18), label: const Text('Réessayer'), style: FilledButton.styleFrom(backgroundColor: ModuleColors.organisations), ), ], ), ), ); } // ─── Actions ────────────────────────────────────────────────────────────── void _showOrganizationDetails(OrganizationModel org) { final orgId = org.id; if (orgId == null || orgId.isEmpty) return; final bloc = context.read(); Navigator.of(context).push(MaterialPageRoute( builder: (_) => BlocProvider.value(value: bloc, child: OrganizationDetailPage(organizationId: orgId)), )); } void _showCreateOrganizationDialog() { final bloc = context.read(); showDialog( context: context, builder: (_) => BlocProvider.value(value: bloc, child: const CreateOrganizationDialog()), ); } void _showEditOrganizationDialog(OrganizationModel org) { final bloc = context.read(); showDialog( context: context, builder: (_) => BlocProvider.value(value: bloc, child: EditOrganizationDialog(organization: org)), ); } void _confirmDeleteOrganization(OrganizationModel org) { final bloc = context.read(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Supprimer l\'organisation'), content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'), actions: [ TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler')), FilledButton( onPressed: () { if (org.id != null) bloc.add(DeleteOrganization(org.id!)); Navigator.of(ctx).pop(); }, style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: AppColors.onError), child: const Text('Supprimer'), ), ], ), ); } }