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'; /// Page de gestion des organisations - Interface sophistiquée et exhaustive /// /// Cette page offre une interface complète pour la gestion des organisations /// avec des fonctionnalités avancées de recherche, filtrage, statistiques /// et actions de gestion basées sur les permissions utilisateur. /// /// **Design System V2** - Utilise UnionFlowColors et composants standardisés /// **Backend connecté** - Toutes les données proviennent d'OrganizationsBloc class OrganizationsPage extends StatefulWidget { const OrganizationsPage({super.key}); @override State createState() => _OrganizationsPageState(); } class _OrganizationsPageState extends State with TickerProviderStateMixin { // Controllers et état final TextEditingController _searchController = TextEditingController(); TabController? _tabController; final ScrollController _scrollController = ScrollController(); List _availableTypes = []; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); // Les organisations sont déjà chargées par OrganizationsPageWrapper // Le TabController sera initialisé dans didChangeDependencies } @override void dispose() { _tabController?.dispose(); _searchController.dispose(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent * 0.9) { // Charger plus d'organisations quand on approche du bas context.read().add(const LoadMoreOrganizations()); } } /// Calcule les types d'organisations disponibles dans les données List _calculateAvailableTypes(List organizations) { if (organizations.isEmpty) { return [null]; // Seulement "Toutes" } // Extraire tous les types uniques final typesSet = organizations.map((org) => org.typeOrganisation).toSet(); final types = typesSet.toList()..sort((a, b) => a.compareTo(b)); // null en premier pour "Toutes", puis les types triés alphabétiquement return [null, ...types]; } /// Initialise ou met à jour le TabController si les types ont changé void _updateTabController(List newTypes) { if (_availableTypes.length != newTypes.length || !_availableTypes.every((type) => newTypes.contains(type))) { _availableTypes = newTypes; _tabController?.dispose(); _tabController = TabController(length: _availableTypes.length, vsync: this); } } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { // Gestion des messages de succès et erreurs if (state is OrganizationsError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: AppColors.error, duration: const Duration(seconds: 4), action: SnackBarAction( label: 'Réessayer', textColor: Colors.white, 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) { return AfricanPatternBackground( child: Scaffold( backgroundColor: Colors.transparent, appBar: UFAppBar( title: 'Gestion des Organisations', backgroundColor: AppColors.lightSurface, foregroundColor: AppColors.textPrimaryLight, elevation: 0, actions: [ 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', ), ], ), body: SafeArea( child: _buildBody(state), ), floatingActionButton: _buildActionButton(state), ), ); }, ); } Widget _buildBody(OrganizationsState state) { if (state is OrganizationsInitial || state is OrganizationsLoading) { return _buildLoadingState(); } if (state is OrganizationsLoaded) { final loadedState = state; // Calculer les types disponibles et mettre à jour le TabController final availableTypes = _calculateAvailableTypes(loadedState.organizations); _updateTabController(availableTypes); return RefreshIndicator( 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: [ // Header avec design system AnimatedSlideIn( duration: const Duration(milliseconds: 500), curve: Curves.easeOut, child: _buildHeader(loadedState), ), const SizedBox(height: SpacingTokens.md), // Section statistiques AnimatedSlideIn( duration: const Duration(milliseconds: 600), curve: Curves.easeOut, child: _buildStatsSection(loadedState), ), const SizedBox(height: SpacingTokens.md), // Barre de recherche AnimatedSlideIn( duration: const Duration(milliseconds: 700), curve: Curves.easeOut, child: _buildSearchBar(loadedState), ), const SizedBox(height: SpacingTokens.md), // Onglets de catégories dynamiques AnimatedSlideIn( duration: const Duration(milliseconds: 800), curve: Curves.easeOut, child: _buildCategoryTabs(availableTypes), ), const SizedBox(height: SpacingTokens.md), // Liste des organisations AnimatedSlideIn( duration: const Duration(milliseconds: 900), curve: Curves.easeOut, child: _buildOrganizationsList(loadedState), ), const SizedBox(height: 80), // Espace pour le FAB ], ), ), ), ); } if (state is OrganizationsLoadingMore) { // Show current organizations with loading indicator at bottom return SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(SpacingTokens.md), physics: const AlwaysScrollableScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLoadingMorePlaceholder(state.currentOrganizations), const Padding( padding: EdgeInsets.all(SpacingTokens.md), child: Center( child: CircularProgressIndicator( color: AppColors.primaryGreen, ), ), ), const SizedBox(height: 80), ], ), ); } if (state is OrganizationsError) { return _buildErrorState(state); } return _buildLoadingState(); } /// Placeholder pour affichage pendant le chargement de plus d'organisations Widget _buildLoadingMorePlaceholder(List currentOrganizations) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: currentOrganizations.length, separatorBuilder: (context, index) => const SizedBox(height: SpacingTokens.sm), itemBuilder: (context, index) { final org = currentOrganizations[index]; return OrganizationCard( organization: org, onTap: () => _showOrganizationDetails(org), showActions: false, ); }, ); } /// Bouton d'action harmonisé avec Design System V2 Widget? _buildActionButton(OrganizationsState state) { // Afficher le FAB seulement si les données sont chargées if (state is! OrganizationsLoaded && state is! OrganizationsLoadingMore) { return null; } // Réservé au Super Admin uniquement final authState = context.read().state; if (authState is! AuthAuthenticated || authState.effectiveRole != UserRole.superAdmin) { return null; } return FloatingActionButton.extended( onPressed: _showCreateOrganizationDialog, backgroundColor: AppColors.primaryGreen, elevation: 8, icon: const Icon(Icons.add, color: Colors.white), label: const Text( 'Nouvelle organisation', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, ), ), ); } /// Header épuré avec Design System V2 Widget _buildHeader(OrganizationsLoaded state) { return Container( padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.brandGreen, AppColors.primaryGreen], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(RadiusTokens.lg), boxShadow: [ BoxShadow( color: AppColors.primaryGreen.withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(SpacingTokens.sm), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(RadiusTokens.md), ), child: const Icon( Icons.business, color: Colors.white, size: 24, ), ), const SizedBox(width: SpacingTokens.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gestion des Organisations', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( '${state.filteredOrganizations.length} organisation(s)', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), ], ), ), IconButton( onPressed: () { context.read().add(const RefreshOrganizations()); }, icon: const Icon(Icons.refresh, color: Colors.white), tooltip: 'Rafraîchir', ), ], ), ); } /// Section statistiques avec données réelles et Design System V2 Widget _buildStatsSection(OrganizationsLoaded state) { final totalOrgs = state.organizations.length; final activeOrgs = state.organizations.where((o) => o.statut == StatutOrganization.active).length; final totalMembers = state.organizations.fold(0, (sum, o) => sum + o.nombreMembres); return Container( padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( color: AppColors.lightSurface, borderRadius: BorderRadius.circular(RadiusTokens.lg), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon( Icons.analytics_outlined, color: AppColors.textSecondaryLight, size: 20, ), const SizedBox(width: SpacingTokens.xs), const Text( 'Statistiques', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimaryLight, ), ), ], ), const SizedBox(height: SpacingTokens.md), Row( children: [ Expanded( child: _buildStatCard( 'Total', totalOrgs.toString(), Icons.business_outlined, AppColors.primaryGreen, ), ), const SizedBox(width: SpacingTokens.sm), Expanded( child: _buildStatCard( 'Actives', activeOrgs.toString(), Icons.check_circle_outline, AppColors.success, ), ), const SizedBox(width: SpacingTokens.sm), Expanded( child: _buildStatCard( 'Membres', totalMembers.toString(), Icons.people_outline, AppColors.primaryGreen, ), ), ], ), ], ), ); } /// Carte de statistique avec Design System V2 Widget _buildStatCard(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(SpacingTokens.sm), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(RadiusTokens.md), border: Border.all( color: color.withOpacity(0.2), width: 1, ), ), child: Column( children: [ Icon(icon, color: color, size: 20), const SizedBox(height: SpacingTokens.xs), Text( value, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimaryLight, ), ), Text( label, style: const TextStyle( fontSize: 12, color: AppColors.textSecondaryLight, fontWeight: FontWeight.w500, ), ), ], ), ); } /// Barre de recherche avec Design System V2 Widget _buildSearchBar(OrganizationsLoaded state) { return Container( padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( color: AppColors.lightSurface, borderRadius: BorderRadius.circular(RadiusTokens.lg), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))], ), child: Container( decoration: BoxDecoration( color: AppColors.lightBackground, borderRadius: BorderRadius.circular(RadiusTokens.md), border: Border.all( color: AppColors.lightBorder, width: 1, ), ), child: TextField( controller: _searchController, onChanged: (value) { context.read().add(SearchOrganizations(value)); }, decoration: InputDecoration( hintText: 'Rechercher par nom, type, localisation...', hintStyle: const TextStyle( color: AppColors.textSecondaryLight, fontSize: 14, ), prefixIcon: const Icon(Icons.search, color: AppColors.primaryGreen), suffixIcon: _searchController.text.isNotEmpty ? IconButton( onPressed: () { _searchController.clear(); context.read().add(const SearchOrganizations('')); }, icon: const Icon(Icons.clear, color: AppColors.textSecondaryLight), ) : null, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: SpacingTokens.md, vertical: SpacingTokens.sm, ), ), ), ), ); } /// Onglets de catégories générés dynamiquement selon les types disponibles Widget _buildCategoryTabs(List availableTypes) { if (_tabController == null || availableTypes.isEmpty) { return const SizedBox.shrink(); } return BlocBuilder( builder: (context, state) { return Container( decoration: BoxDecoration( color: AppColors.lightSurface, borderRadius: BorderRadius.circular(RadiusTokens.lg), boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))], ), child: TabBar( controller: _tabController!, isScrollable: availableTypes.length > 4, // Scrollable si plus de 4 types labelColor: AppColors.primaryGreen, unselectedLabelColor: AppColors.textSecondaryLight, indicatorColor: AppColors.primaryGreen, indicatorWeight: 3, indicatorSize: TabBarIndicatorSize.tab, labelStyle: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), unselectedLabelStyle: const TextStyle( fontWeight: FontWeight.normal, fontSize: 14, ), onTap: (index) { // Filtrer par type selon l'onglet sélectionné final selectedType = availableTypes[index]; if (selectedType != null) { context.read().add(FilterOrganizationsByType(selectedType)); } else { // null = "Toutes" → effacer les filtres context.read().add(const ClearOrganizationsFilters()); } }, tabs: availableTypes.map((type) { final label = type == null ? 'Toutes' : type; return Tab(text: label); }).toList(), ), ); }, ); } /// Liste des organisations avec données réelles et OrganizationCard Widget _buildOrganizationsList(OrganizationsLoaded state) { final organizations = state.filteredOrganizations; if (organizations.isEmpty) { return _buildEmptyState(); } // Vérifier le rôle une seule fois pour toute la liste (UI-02) 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: (context, index) => const SizedBox(height: SpacingTokens.sm), itemBuilder: (context, index) { final org = organizations[index]; return AnimatedFadeIn( duration: Duration(milliseconds: 300 + (index * 50)), child: OrganizationCard( organization: org, onTap: () => _showOrganizationDetails(org), onEdit: canManageOrgs ? () => _showEditOrganizationDialog(org) : null, onDelete: canManageOrgs ? () => _confirmDeleteOrganization(org) : null, showActions: canManageOrgs, ), ); }, ); } /// État vide avec Design System V2 Widget _buildEmptyState() { return Container( padding: const EdgeInsets.all(SpacingTokens.xl), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( color: AppColors.primaryGreen.withOpacity(0.12), shape: BoxShape.circle, ), child: const Icon( Icons.business_outlined, size: 64, color: AppColors.primaryGreen, ), ), const SizedBox(height: SpacingTokens.lg), const Text( 'Aucune organisation trouvée', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimaryLight, ), ), const SizedBox(height: SpacingTokens.xs), const Text( 'Essayez de modifier vos critères de recherche\nou créez une nouvelle organisation', style: TextStyle( fontSize: 14, color: AppColors.textSecondaryLight, ), textAlign: TextAlign.center, ), const SizedBox(height: SpacingTokens.lg), ElevatedButton.icon( onPressed: () { context.read().add(const ClearOrganizationsFilters()); _searchController.clear(); }, icon: const Icon(Icons.clear_all), label: const Text('Réinitialiser les filtres'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.lg, vertical: SpacingTokens.sm, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(RadiusTokens.md), ), ), ), ], ), ), ); } /// État de chargement avec Design System V2 Widget _buildLoadingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( color: AppColors.primaryGreen, strokeWidth: 3, ), const SizedBox(height: SpacingTokens.md), const Text( 'Chargement des organisations...', style: TextStyle( fontSize: 14, color: AppColors.textSecondaryLight, ), ), ], ), ); } /// État d'erreur avec Design System V2 Widget _buildErrorState(OrganizationsError state) { return Center( child: Container( margin: const EdgeInsets.all(SpacingTokens.xl), padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( color: AppColors.error.withOpacity(0.08), borderRadius: BorderRadius.circular(RadiusTokens.lg), border: Border.all( color: AppColors.error.withOpacity(0.3), width: 1, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.error_outline, size: 64, color: AppColors.error, ), const SizedBox(height: SpacingTokens.md), Text( state.message, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimaryLight, ), textAlign: TextAlign.center, ), if (state.details != null) ...[ const SizedBox(height: SpacingTokens.xs), Text( state.details!, style: const TextStyle( fontSize: 12, color: AppColors.textSecondaryLight, ), textAlign: TextAlign.center, ), ], const SizedBox(height: SpacingTokens.lg), ElevatedButton.icon( onPressed: () { context.read().add(const LoadOrganizations(refresh: true)); }, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: SpacingTokens.lg, vertical: SpacingTokens.sm, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(RadiusTokens.md), ), ), ), ], ), ), ); } // Méthodes d'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: (dialogContext) => AlertDialog( title: const Text('Supprimer l\'organisation'), content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { if (org.id != null) { bloc.add(DeleteOrganization(org.id!)); } Navigator.of(dialogContext).pop(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.error, foregroundColor: Colors.white, ), child: const Text('Supprimer'), ), ], ), ); } }