diff --git a/lib/core/navigation/more_page.dart b/lib/core/navigation/more_page.dart index 54e49dd..22b1620 100644 --- a/lib/core/navigation/more_page.dart +++ b/lib/core/navigation/more_page.dart @@ -8,10 +8,6 @@ import '../../shared/widgets/mini_avatar.dart'; import '../di/injection_container.dart'; import '../network/org_context_service.dart'; -import '../../features/admin/presentation/pages/user_management_page.dart'; -import '../../features/settings/presentation/pages/system_settings_page.dart'; -import '../../features/backup/presentation/pages/backup_page.dart'; -import '../../features/logs/presentation/pages/logs_page.dart'; import '../../features/reports/presentation/pages/reports_page_wrapper.dart'; import '../../features/epargne/presentation/pages/epargne_page.dart'; import '../../features/contributions/presentation/pages/contributions_page_wrapper.dart' show CotisationsPageWrapper; @@ -20,6 +16,7 @@ import '../../features/solidarity/presentation/pages/demandes_aide_page_wrapper. import '../../features/organizations/presentation/pages/organizations_page_wrapper.dart'; import '../../features/organizations/presentation/pages/org_selector_page.dart'; import '../../features/organizations/bloc/org_switcher_bloc.dart'; +import '../../shared/design_system/tokens/app_colors.dart'; import '../../features/profile/presentation/pages/profile_page_wrapper.dart'; /// Page "Plus" avec les fonctions avancées selon le rôle et les modules actifs. @@ -39,10 +36,10 @@ class MorePage extends StatelessWidget { } return Scaffold( - backgroundColor: AppColors.lightBackground, appBar: const UFAppBar( title: 'PLUS', automaticallyImplyLeading: false, + moduleGradient: ModuleColors.systemeGradient, ), body: SingleChildScrollView( padding: const EdgeInsets.all(12), @@ -65,9 +62,7 @@ class MorePage extends StatelessWidget { Widget _buildUserProfile( BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final textColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; - final roleColor = isDark ? AppColors.brandGreenLight : AppColors.primaryGreen; + final scheme = Theme.of(context).colorScheme; return CoreCard( onTap: () => Navigator.of(context).push( @@ -89,12 +84,12 @@ class MorePage extends StatelessWidget { children: [ Text( '${state.user.firstName} ${state.user.lastName}', - style: AppTypography.actionText.copyWith(color: textColor), + style: AppTypography.actionText.copyWith(color: scheme.onSurface), ), Text( state.effectiveRole.displayName.toUpperCase(), style: AppTypography.badgeText.copyWith( - color: roleColor, + color: ModuleColors.profil, fontWeight: FontWeight.bold, ), ), @@ -105,20 +100,16 @@ class MorePage extends StatelessWidget { ], ), ), - Icon(Icons.chevron_right, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, - size: 16), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 16), ], ), ); } void _openOrgSelector(BuildContext context) { - // Vérifier que OrgSwitcherBloc est disponible (fourni par un ancêtre) try { showOrgSelector(context); } catch (_) { - // OrgSwitcherBloc pas fourni dans ce contexte, navigation vers ProfilePage Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ProfilePageWrapper()), ); @@ -129,48 +120,24 @@ class MorePage extends StatelessWidget { BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) { final options = []; - if (state.effectiveRole == UserRole.superAdmin) { - options.addAll([ - _buildSectionTitle(context, 'Administration Système'), - _buildOptionTile(context, - icon: Icons.people, - title: 'Gestion des utilisateurs', - subtitle: 'Utilisateurs Keycloak et rôles', - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const UserManagementPage())), - ), - _buildOptionTile(context, - icon: Icons.settings, - title: 'Paramètres Système', - subtitle: 'Configuration globale', - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SystemSettingsPage())), - ), - _buildOptionTile(context, - icon: Icons.backup, - title: 'Sauvegarde & Restauration', - subtitle: 'Gestion des sauvegardes', - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const BackupPage())), - ), - _buildOptionTile(context, - icon: Icons.article, - title: 'Logs & Monitoring', - subtitle: 'Surveillance et journaux', - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const LogsPage())), - ), - ]); - } + // Note: les items SYSTÈME (Gestion utilisateurs, Paramètres Système, + // Sauvegarde & Restauration, Logs & Monitoring) ont été déplacés dans + // le burger drawer (section "Système" — super admin only) afin de + // respecter la démarcation Métier (Plus) / Système (Drawer). if (state.effectiveRole == UserRole.orgAdmin || state.effectiveRole == UserRole.superAdmin) { + final isSuperAdmin = state.effectiveRole == UserRole.superAdmin; + final orgTitle = _orgTitle(context, isSuperAdmin); + final orgSubtitle = _orgSubtitle(context, isSuperAdmin); + options.addAll([ _buildSectionTitle(context, 'Administration'), _buildOptionTile(context, icon: Icons.business, - title: 'Gestion des Organisations', - subtitle: 'Créer et gérer les organisations', + title: orgTitle, + subtitle: orgSubtitle, + accentColor: ModuleColors.organisations, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const OrganizationsPageWrapper())), ), @@ -179,12 +146,14 @@ class MorePage extends StatelessWidget { icon: Icons.pending_actions, title: 'Approbations en attente', subtitle: 'Valider les transactions financières', + accentColor: ModuleColors.financeWorkflow, onTap: () => Navigator.pushNamed(context, '/approvals'), ), _buildOptionTile(context, icon: Icons.account_balance_wallet, title: 'Gestion des Budgets', subtitle: 'Créer et suivre les budgets', + accentColor: ModuleColors.financeWorkflow, onTap: () => Navigator.pushNamed(context, '/budgets'), ), _buildSectionTitle(context, 'Communication'), @@ -192,6 +161,7 @@ class MorePage extends StatelessWidget { icon: Icons.message, title: 'Messages & Broadcast', subtitle: 'Communiquer avec les membres', + accentColor: ModuleColors.communication, onTap: () => Navigator.pushNamed(context, '/messages'), ), _buildSectionTitle(context, 'Rapports & Analytics'), @@ -199,6 +169,7 @@ class MorePage extends StatelessWidget { icon: Icons.assessment, title: 'Rapports & Analytics', subtitle: 'Statistiques détaillées', + accentColor: ModuleColors.rapports, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ReportsPageWrapper())), ), @@ -212,6 +183,7 @@ class MorePage extends StatelessWidget { icon: Icons.message, title: 'Messages aux membres', subtitle: 'Communiquer avec les membres', + accentColor: ModuleColors.communication, onTap: () => Navigator.pushNamed(context, '/messages'), ), ]); @@ -227,101 +199,92 @@ class MorePage extends StatelessWidget { final isAdmin = state.effectiveRole == UserRole.orgAdmin || state.effectiveRole == UserRole.superAdmin; - // Module TONTINE if (orgCtx.isModuleActif('TONTINE')) { options.add(_buildSectionTitle(context, 'Tontine')); - if (isAdmin) { - options.add(_buildOptionTile(context, - icon: Icons.autorenew, - title: 'Gestion Tontine', - subtitle: 'Cycles, cotisations et remises', - onTap: () => Navigator.pushNamed(context, '/tontine'), - )); - } else { - options.add(_buildOptionTile(context, - icon: Icons.autorenew, - title: 'Ma Tontine', - subtitle: 'Mes cycles et cotisations', - onTap: () => Navigator.pushNamed(context, '/tontine'), - )); - } + options.add(_buildOptionTile(context, + icon: Icons.autorenew, + title: isAdmin ? 'Gestion Tontine' : 'Ma Tontine', + subtitle: isAdmin ? 'Cycles, cotisations et remises' : 'Mes cycles et cotisations', + accentColor: ModuleColors.cotisations, + onTap: () => _comingSoon(context, 'Tontine'), + )); } - // Module EPARGNE if (orgCtx.isModuleActif('EPARGNE')) { options.add(_buildSectionTitle(context, 'Épargne')); options.add(_buildOptionTile(context, icon: Icons.savings, title: isAdmin ? 'Gestion Épargne' : 'Mon Épargne', - subtitle: isAdmin ? 'Comptes épargne et transactions' : 'Mon compte épargne', + subtitle: isAdmin ? 'Épargne, dépôts, crédits et transactions' : 'Mon épargne et mes crédits', + accentColor: ModuleColors.epargne, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const EpargnePage())), )); } - // Module CREDIT if (orgCtx.isModuleActif('CREDIT')) { options.add(_buildSectionTitle(context, 'Crédit')); options.add(_buildOptionTile(context, icon: Icons.account_balance, title: isAdmin ? 'Gestion Crédit' : 'Mon Crédit', subtitle: isAdmin ? 'Demandes et suivi des crédits' : 'Mes demandes de crédit', - onTap: () => Navigator.pushNamed(context, '/credit'), + accentColor: ModuleColors.financeWorkflow, + onTap: () => _comingSoon(context, 'Crédit'), )); } - // Module AGRICULTURE if (orgCtx.isModuleActif('AGRICULTURE')) { options.add(_buildSectionTitle(context, 'Agriculture')); options.add(_buildOptionTile(context, icon: Icons.eco, title: 'Campagnes Agricoles', subtitle: 'Parcelles, récoltes et stocks', - onTap: () => Navigator.pushNamed(context, '/agricole'), + accentColor: ModuleColors.epargne, + onTap: () => _comingSoon(context, 'Agriculture'), )); } - // Module COLLECTE_FONDS if (orgCtx.isModuleActif('COLLECTE_FONDS')) { options.add(_buildSectionTitle(context, 'Collecte de Fonds')); options.add(_buildOptionTile(context, icon: Icons.volunteer_activism, title: 'Campagnes de Collecte', subtitle: 'Dons et levées de fonds', - onTap: () => Navigator.pushNamed(context, '/collecte'), + accentColor: ModuleColors.solidarite, + onTap: () => _comingSoon(context, 'Collecte de Fonds'), )); } - // Module PROJETS_ONG if (orgCtx.isModuleActif('PROJETS_ONG')) { options.add(_buildSectionTitle(context, 'Projets ONG')); options.add(_buildOptionTile(context, icon: Icons.public, title: 'Projets', subtitle: 'Gérer et suivre les projets', - onTap: () => Navigator.pushNamed(context, '/projets-ong'), + accentColor: ModuleColors.rapports, + onTap: () => _comingSoon(context, 'Projets ONG'), )); } - // Module CULTE_DONS if (orgCtx.isModuleActif('CULTE_DONS')) { options.add(_buildSectionTitle(context, 'Culte & Dons')); options.add(_buildOptionTile(context, icon: Icons.church, title: 'Dons et Offrandes', subtitle: 'Gestion des dons religieux', - onTap: () => Navigator.pushNamed(context, '/culte'), + accentColor: ModuleColors.solidarite, + onTap: () => _comingSoon(context, 'Culte & Dons'), )); } - // Module VOTES if (orgCtx.isModuleActif('VOTES')) { options.add(_buildSectionTitle(context, 'Votes')); options.add(_buildOptionTile(context, icon: Icons.how_to_vote, title: 'Votes & Élections', subtitle: 'Campagnes et résultats', - onTap: () => Navigator.pushNamed(context, '/votes'), + accentColor: ModuleColors.financeWorkflow, + onTap: () => _comingSoon(context, 'Votes & Élections'), )); } @@ -335,6 +298,7 @@ class MorePage extends StatelessWidget { icon: Icons.payment, title: 'Cotisations', subtitle: 'Gérer les cotisations', + accentColor: ModuleColors.cotisations, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const CotisationsPageWrapper())), ), @@ -342,6 +306,7 @@ class MorePage extends StatelessWidget { icon: Icons.how_to_reg, title: 'Demandes d\'adhésion', subtitle: 'Demandes d\'adhésion à une organisation', + accentColor: ModuleColors.adhesions, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const AdhesionsPageWrapper())), ), @@ -349,24 +314,64 @@ class MorePage extends StatelessWidget { icon: Icons.volunteer_activism, title: 'Demandes d\'aide', subtitle: 'Solidarité – demandes d\'aide', + accentColor: ModuleColors.solidarite, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const DemandesAidePageWrapper())), ), - // Épargne — affiché en commun uniquement si le module n'est PAS actif (évite doublon avec section module) if (!orgCtx.isModuleActif('EPARGNE')) _buildOptionTile(context, icon: Icons.savings_outlined, - title: 'Comptes épargne', - subtitle: 'Mutuelle épargne – dépôts (LCB-FT)', + title: 'Épargne & Crédit', + subtitle: 'Comptes épargne, dépôts et crédits (LCB-FT)', + accentColor: ModuleColors.epargne, onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const EpargnePage())), ), ]; } - Widget _buildSectionTitle(BuildContext context, String title) { + // ─── Helpers titre organisation ───────────────────────────────────────── + /// Titre de l'entrée "Organisation(s)" selon le rôle et le nombre d'orgs gérées. + String _orgTitle(BuildContext context, bool isSuperAdmin) { + if (isSuperAdmin) return 'Gestion des Organisations'; + final switcherState = context.read().state; + final count = switcherState is OrgSwitcherLoaded + ? switcherState.organisations.length + : 1; + return count > 1 ? 'Mes Organisations' : 'Mon Organisation'; + } + + String _orgSubtitle(BuildContext context, bool isSuperAdmin) { + if (isSuperAdmin) return 'Créer et gérer les organisations'; + final switcherState = context.read().state; + final count = switcherState is OrgSwitcherLoaded + ? switcherState.organisations.length + : 1; + return count > 1 ? 'Gérer mes organisations' : 'Gérer mon organisation'; + } + + // ─── Modules non encore implémentés ───────────────────────────────────── + + void _comingSoon(BuildContext context, String feature) { final isDark = Theme.of(context).brightness == Brightness.dark; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row(children: [ + const Icon(Icons.construction_outlined, color: Colors.white, size: 16), + const SizedBox(width: 8), + Expanded(child: Text('$feature — Disponible prochainement')), + ]), + backgroundColor: isDark ? AppColors.surfaceVariantDark : AppColors.textSecondary, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 3), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + } + + Widget _buildSectionTitle(BuildContext context, String title) { + final scheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(top: 16, bottom: 6, left: 4), child: Text( @@ -374,7 +379,7 @@ class MorePage extends StatelessWidget { style: AppTypography.subtitleSmall.copyWith( fontWeight: FontWeight.bold, letterSpacing: 1.1, - color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, + color: scheme.onSurfaceVariant, ), ), ); @@ -388,16 +393,8 @@ class MorePage extends StatelessWidget { required VoidCallback onTap, Color? accentColor, }) { - - final isDark = Theme.of(context).brightness == Brightness.dark; - final accent = accentColor ?? AppColors.primaryGreen; - final titleColor = accentColor != null - ? accentColor - : (isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight); - final subtitleColor = - isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; - final chevronColor = - isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; + final scheme = Theme.of(context).colorScheme; + final accent = accentColor ?? scheme.primary; return CoreCard( margin: const EdgeInsets.only(bottom: 8), @@ -407,7 +404,7 @@ class MorePage extends StatelessWidget { Container( padding: const EdgeInsets.all(7), decoration: BoxDecoration( - color: accent.withOpacity(isDark ? 0.2 : 0.1), + color: accent.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: accent, size: 18), @@ -419,16 +416,18 @@ class MorePage extends StatelessWidget { children: [ Text( title, - style: AppTypography.actionText.copyWith(color: titleColor), + style: AppTypography.actionText.copyWith( + color: scheme.onSurface, + ), ), Text( subtitle, - style: AppTypography.subtitleSmall.copyWith(color: subtitleColor), + style: AppTypography.subtitleSmall.copyWith(color: scheme.onSurfaceVariant), ), ], ), ), - Icon(Icons.chevron_right, color: chevronColor, size: 16), + Icon(Icons.chevron_right, color: scheme.onSurfaceVariant, size: 16), ], ), ); diff --git a/lib/features/organizations/presentation/pages/organizations_page.dart b/lib/features/organizations/presentation/pages/organizations_page.dart index 15bf4df..f33ef5f 100644 --- a/lib/features/organizations/presentation/pages/organizations_page.dart +++ b/lib/features/organizations/presentation/pages/organizations_page.dart @@ -16,15 +16,10 @@ import '../../../../shared/design_system/components/african_pattern_background.d 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 -/// -/// 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}); @@ -33,18 +28,21 @@ class OrganizationsPage extends StatefulWidget { } class _OrganizationsPageState extends State with TickerProviderStateMixin { - // Controllers et état final TextEditingController _searchController = TextEditingController(); TabController? _tabController; final ScrollController _scrollController = ScrollController(); List _availableTypes = []; + /// 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); - // Les organisations sont déjà chargées par OrganizationsPageWrapper - // Le TabController sera initialisé dans didChangeDependencies } @override @@ -57,29 +55,40 @@ class _OrganizationsPageState extends State with TickerProvid 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" + /// 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'; + } - // 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 + 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]; } - /// 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))) { + if (_availableTypes.length != newTypes.length || !_availableTypes.every((t) => newTypes.contains(t))) { _availableTypes = newTypes; _tabController?.dispose(); _tabController = TabController(length: _availableTypes.length, vsync: this); @@ -90,162 +99,120 @@ class _OrganizationsPageState extends State with TickerProvid 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)); - }, - ), + 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), - ), - ); + 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), - ), - ); + 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), - ), - ); + 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: 'Gestion des Organisations', - backgroundColor: AppColors.lightSurface, - foregroundColor: AppColors.textPrimaryLight, + title: _appBarTitle(context), + moduleGradient: ModuleColors.organisationsGradient, elevation: 0, actions: [ IconButton( icon: const Icon(Icons.category_outlined), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const OrgTypesPage(), - ), - ); - }, + 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()); - }, + 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(state), - ), - floatingActionButton: _buildActionButton(state), + body: SafeArea(child: _buildBody(context, state)), + floatingActionButton: _buildActionButton(context, state), ), ); }, ); } - Widget _buildBody(OrganizationsState 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) { - return _buildLoadingState(); + // 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) { - 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 - ], - ), - ), - ), - ); + return _buildLoadedBody(context, state); } - if (state is OrganizationsLoadingMore) { - // Show current organizations with loading indicator at bottom return SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.all(SpacingTokens.md), @@ -253,373 +220,195 @@ class _OrganizationsPageState extends State with TickerProvid child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildLoadingMorePlaceholder(state.currentOrganizations), - const Padding( - padding: EdgeInsets.all(SpacingTokens.md), - child: Center( - child: CircularProgressIndicator( - color: AppColors.primaryGreen, - ), - ), + _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); - if (state is OrganizationsError) { - return _buildErrorState(state); - } - - return _buildLoadingState(); + // É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); } - /// 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, + /// 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.md), + AnimatedSlideIn( + duration: const Duration(milliseconds: 800), + curve: Curves.easeOut, + child: _buildOrganizationsList(context, state), + ), + const SizedBox(height: 80), + ], + ), ), ), ); } - /// Header épuré avec Design System V2 - Widget _buildHeader(OrganizationsLoaded state) { + // ─── 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.all(SpacingTokens.md), + padding: const EdgeInsets.fromLTRB(12, 8, 12, 10), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppColors.brandGreen, AppColors.primaryGreen], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(RadiusTokens.lg), - boxShadow: [ - BoxShadow( - color: AppColors.primaryGreen.withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], + 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: [ - 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', - ), + _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), ], ), ); } - /// 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))], - ), + Widget _kpiTile(BuildContext context, String label, String value, Color color, IconData icon) { + return Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, 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, - ), - ), - ], - ), + 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), ], ), ); } - /// 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, - ), - ), + Widget _kpiTileProgress(BuildContext context, String label, String value, Color color, double progress) { + return Expanded( child: Column( + mainAxisSize: MainAxisSize.min, 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, + 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), ], ), ); } - /// Barre de recherche avec Design System V2 - Widget _buildSearchBar(OrganizationsLoaded state) { + 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( - padding: const EdgeInsets.all(SpacingTokens.md), decoration: BoxDecoration( - color: AppColors.lightSurface, + color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(RadiusTokens.lg), - boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))], + border: Border.all(color: Theme.of(context).colorScheme.outlineVariant, width: 1), + boxShadow: const [BoxShadow(color: AppColors.shadow, 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, - ), - ), + 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), ), ), ); } - /// 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(); - } + // ─── Liste des organisations ────────────────────────────────────────────── - 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) { + Widget _buildOrganizationsList(BuildContext context, OrganizationsLoaded state) { final organizations = state.filteredOrganizations; + if (organizations.isEmpty) return _buildEmptyState(context); - 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); + (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]; + separatorBuilder: (_, __) => const SizedBox(height: SpacingTokens.sm), + itemBuilder: (context, i) { + final org = organizations[i]; return AnimatedFadeIn( - duration: Duration(milliseconds: 300 + (index * 50)), + duration: Duration(milliseconds: 300 + (i * 50)), child: OrganizationCard( organization: org, onTap: () => _showOrganizationDetails(org), @@ -632,63 +421,71 @@ class _OrganizationsPageState extends State with TickerProvid ); } - /// État vide avec Design System V2 - Widget _buildEmptyState() { - return Container( - padding: const EdgeInsets.all(SpacingTokens.xl), - child: Center( + 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: AppColors.primaryGreen.withOpacity(0.12), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.business_outlined, - size: 64, - color: AppColors.primaryGreen, - ), + 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), - const Text( + Text( 'Aucune organisation trouvée', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: AppColors.textPrimaryLight, - ), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface), ), 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, - ), + 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), - ElevatedButton.icon( + FilledButton.icon( onPressed: () { context.read().add(const ClearOrganizationsFilters()); _searchController.clear(); }, - icon: const Icon(Icons.clear_all), + icon: const Icon(Icons.filter_list_off, size: 18), 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), - ), - ), + style: FilledButton.styleFrom(backgroundColor: ModuleColors.organisations), ), ], ), @@ -696,90 +493,57 @@ class _OrganizationsPageState extends State with TickerProvid ); } - /// État de chargement avec Design System V2 - Widget _buildLoadingState() { + // ─── Loading state ──────────────────────────────────────────────────────── + + Widget _buildLoadingState(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator( - color: AppColors.primaryGreen, - strokeWidth: 3, - ), + const CircularProgressIndicator(color: ModuleColors.organisations, strokeWidth: 3), const SizedBox(height: SpacingTokens.md), - const Text( + Text( 'Chargement des organisations...', - style: TextStyle( - fontSize: 14, - color: AppColors.textSecondaryLight, - ), + style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), ); } - /// État d'erreur avec Design System V2 - Widget _buildErrorState(OrganizationsError state) { + // ─── 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: AppColors.error.withOpacity(0.08), + color: cs.errorContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(RadiusTokens.lg), - border: Border.all( - color: AppColors.error.withOpacity(0.3), - width: 1, - ), + border: Border.all(color: cs.error.withOpacity(0.3), width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.error_outline, - size: 64, - color: AppColors.error, - ), + Icon(Icons.error_outline, size: 56, color: cs.error), const SizedBox(height: SpacingTokens.md), Text( state.message, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: AppColors.textPrimaryLight, - ), + 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: const TextStyle( - fontSize: 12, - color: AppColors.textSecondaryLight, - ), - textAlign: TextAlign.center, - ), + Text(state.details!, style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), textAlign: TextAlign.center), ], const SizedBox(height: SpacingTokens.lg), - ElevatedButton.icon( - onPressed: () { - context.read().add(const LoadOrganizations(refresh: true)); - }, - icon: const Icon(Icons.refresh), + FilledButton.icon( + onPressed: () => context.read().add(const LoadOrganizations(refresh: true)), + icon: const Icon(Icons.refresh, size: 18), 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), - ), - ), + style: FilledButton.styleFrom(backgroundColor: ModuleColors.organisations), ), ], ), @@ -787,29 +551,22 @@ class _OrganizationsPageState extends State with TickerProvid ); } - // Méthodes d'actions + // ─── 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), - ), - ), - ); + 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(), - ), + builder: (_) => BlocProvider.value(value: bloc, child: const CreateOrganizationDialog()), ); } @@ -817,10 +574,7 @@ class _OrganizationsPageState extends State with TickerProvid final bloc = context.read(); showDialog( context: context, - builder: (_) => BlocProvider.value( - value: bloc, - child: EditOrganizationDialog(organization: org), - ), + builder: (_) => BlocProvider.value(value: bloc, child: EditOrganizationDialog(organization: org)), ); } @@ -828,25 +582,17 @@ class _OrganizationsPageState extends State with TickerProvid final bloc = context.read(); showDialog( context: context, - builder: (dialogContext) => AlertDialog( + builder: (ctx) => 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( + TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler')), + FilledButton( onPressed: () { - if (org.id != null) { - bloc.add(DeleteOrganization(org.id!)); - } - Navigator.of(dialogContext).pop(); + if (org.id != null) bloc.add(DeleteOrganization(org.id!)); + Navigator.of(ctx).pop(); }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.error, - foregroundColor: Colors.white, - ), + style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: AppColors.onError), child: const Text('Supprimer'), ), ],