diff --git a/lib/features/dashboard/presentation/pages/role_dashboards/org_admin_dashboard.dart b/lib/features/dashboard/presentation/pages/role_dashboards/org_admin_dashboard.dart index c67b78b..7ccebfb 100644 --- a/lib/features/dashboard/presentation/pages/role_dashboards/org_admin_dashboard.dart +++ b/lib/features/dashboard/presentation/pages/role_dashboards/org_admin_dashboard.dart @@ -23,7 +23,7 @@ class OrgAdminDashboard extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: UnionFlowColors.background, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: _buildAppBar(context), drawer: DashboardDrawer( onLogout: () => context.read().add(const AuthLogoutRequested()), @@ -38,7 +38,8 @@ class OrgAdminDashboard extends StatelessWidget { return BlocBuilder( builder: (context, dashboardState) { - if (dashboardState is DashboardLoading) { + if (dashboardState is DashboardLoading || + dashboardState is DashboardInitial) { return const Center( child: CircularProgressIndicator(color: UnionFlowColors.gold), ); @@ -285,43 +286,62 @@ class OrgAdminDashboard extends StatelessWidget { } PreferredSizeWidget _buildAppBar(BuildContext context) { + // Pattern aligné sur SuperAdminDashboard : AppBar + flexibleSpace gradient, + // texte/icônes en blanc sur le gradient. Conserve l'identité "Admin Org" + // via le badge « A » et le gradient gold spécifique au rôle. return AppBar( - backgroundColor: UnionFlowColors.surface, + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, elevation: 0, + scrolledUnderElevation: 0, // Pas de surface tint sur le gradient gold + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: UnionFlowColors.goldGradient, + ), + ), + leading: Builder( + builder: (ctx) => IconButton( + icon: const Icon(Icons.menu, size: 22, color: Colors.white), + onPressed: () => Scaffold.of(ctx).openDrawer(), + ), + ), title: Row( children: [ Container( - width: 32, - height: 32, + width: 28, + height: 28, decoration: BoxDecoration( - gradient: UnionFlowColors.goldGradient, - borderRadius: BorderRadius.circular(8), + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.white.withOpacity(0.4)), ), alignment: Alignment.center, child: const Text( 'A', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 18), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 15), ), ), - const SizedBox(width: 12), + const SizedBox(width: 8), const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('UnionFlow', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary)), - Text('Admin Organisation', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w400, color: UnionFlowColors.textSecondary)), + Text('UnionFlow', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)), + Text('Admin Organisation', + style: TextStyle(fontSize: 10, fontWeight: FontWeight.w400, color: Colors.white70)), ], ), ], ), - iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary), + iconTheme: const IconThemeData(color: Colors.white), + actionsIconTheme: const IconThemeData(color: Colors.white), actions: [ UnionExportButton(onExport: (_) => _handleExport(context)), const SizedBox(width: 8), UnionNotificationBadge( count: 0, child: IconButton( - icon: const Icon(Icons.notifications_outlined), - color: UnionFlowColors.textPrimary, + icon: const Icon(Icons.notifications_outlined, color: Colors.white), onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const NotificationsPageWrapper()), ), diff --git a/lib/features/dashboard/presentation/pages/role_dashboards/super_admin_dashboard.dart b/lib/features/dashboard/presentation/pages/role_dashboards/super_admin_dashboard.dart index ac48d06..a31bfdc 100644 --- a/lib/features/dashboard/presentation/pages/role_dashboards/super_admin_dashboard.dart +++ b/lib/features/dashboard/presentation/pages/role_dashboards/super_admin_dashboard.dart @@ -1,6 +1,10 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../shared/design_system/unionflow_design_v2.dart'; +import '../../../../../shared/design_system/tokens/module_colors.dart'; +import '../../../../../shared/design_system/tokens/color_tokens.dart'; import '../../bloc/dashboard_bloc.dart'; import '../../../domain/entities/dashboard_entity.dart'; import '../../../../authentication/presentation/bloc/auth_bloc.dart'; @@ -11,252 +15,302 @@ import '../../../../backup/presentation/pages/backup_page.dart'; import '../../../../help/presentation/pages/help_support_page.dart'; import '../../widgets/dashboard_drawer.dart'; -/// Dashboard Super Admin — design fintech compact -class SuperAdminDashboard extends StatelessWidget { +/// Dashboard Super Admin — design harmonisé v2 +/// +/// Corrections v2 : +/// • Dark mode : _MemberActivityCard / _OrgTypeCard en StatelessWidget avec BuildContext +/// • Couleur module : ModuleColors.systeme (navy autorité) au lieu de UnionFlowColors.error +/// • ROOT badge : ModuleColors.solidariteGradient (rouge sémantique = root/danger) +/// • KPIs : ModuleColors.organisations/membres/evenements/cotisations +/// • Indicateur WebSocket dans l'AppBar +/// • États vides pour activités et événements +/// • Refresh via Completer (plus de Future.delayed artificiel) +/// • "Sécurité" → "Analytics" (supprime la duplication avec "Système") +class SuperAdminDashboard extends StatefulWidget { const SuperAdminDashboard({super.key}); + @override + State createState() => _SuperAdminDashboardState(); +} + +class _SuperAdminDashboardState extends State { + Completer? _refreshCompleter; + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: UnionFlowColors.background, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: _buildAppBar(context), drawer: DashboardDrawer( onLogout: () => context.read().add(const AuthLogoutRequested()), ), - body: BlocBuilder( - builder: (context, authState) { - final user = (authState is AuthAuthenticated) ? authState.user : null; - return BlocBuilder( - builder: (context, dashboardState) { - if (dashboardState is DashboardLoading) { - return const Center( - child: CircularProgressIndicator( - color: UnionFlowColors.error, - strokeWidth: 2, - ), - ); - } - final dashboardData = (dashboardState is DashboardLoaded) - ? dashboardState.dashboardData - : null; - final stats = dashboardData?.stats; + body: BlocListener( + listener: (context, state) { + if (state is DashboardLoaded || state is DashboardError) { + _refreshCompleter?.complete(); + _refreshCompleter = null; + } + }, + child: BlocBuilder( + builder: (context, authState) { + final user = (authState is AuthAuthenticated) ? authState.user : null; + return BlocBuilder( + builder: (context, dashboardState) { + if (dashboardState is DashboardLoading || + dashboardState is DashboardInitial) { + return const Center( + child: CircularProgressIndicator( + color: ModuleColors.systeme, + strokeWidth: 2, + ), + ); + } - return RefreshIndicator( - color: UnionFlowColors.error, - strokeWidth: 2, - onRefresh: () async { - context.read().add(LoadDashboardData( - organizationId: dashboardData?.organizationId ?? '', - userId: user?.id ?? '', - useGlobalDashboard: true, - )); - await Future.delayed(const Duration(milliseconds: 600)); - }, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ── Carte identité ─────────────────────────────── - UserIdentityCard( - initials: user?.initials ?? 'SA', - name: user?.fullName ?? 'Super Administrateur', - subtitle: 'Accès global système', - badgeLabel: 'ROOT', - gradient: const LinearGradient( - colors: [Color(0xFFB91C1C), Color(0xFF7F1D1D)], - begin: Alignment.centerLeft, - end: Alignment.centerRight, + final dashboardData = dashboardState is DashboardLoaded + ? dashboardState.dashboardData + : dashboardState is DashboardRefreshing + ? dashboardState.dashboardData + : null; + final stats = dashboardData?.stats; + + return RefreshIndicator( + color: ModuleColors.systeme, + strokeWidth: 2, + onRefresh: () async { + _refreshCompleter = Completer(); + context.read().add(LoadDashboardData( + organizationId: dashboardData?.organizationId ?? '', + userId: user?.id ?? '', + useGlobalDashboard: true, + )); + return _refreshCompleter!.future; + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ── Carte identité ROOT ────────────────────────── + UserIdentityCard( + initials: user?.initials ?? 'SA', + name: user?.fullName ?? 'Super Administrateur', + subtitle: 'Accès global système', + badgeLabel: 'ROOT', + gradient: LinearGradient( + colors: ModuleColors.solidariteGradient, + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + accentColor: ModuleColors.solidarite, + showTopBorder: false, ), - accentColor: UnionFlowColors.error, - showTopBorder: false, - ), - const SizedBox(height: 12), + const SizedBox(height: 12), - // ── Balance principale ─────────────────────────── - UnionBalanceCard( - label: 'Caisse Globale Système', - amount: _formatAmount(stats?.totalContributionAmount ?? 0), - trend: stats != null && stats.totalContributionAmount > 0 - ? '${stats.monthlyGrowth >= 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}%' - : null, - isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0, - ), - const SizedBox(height: 12), + // ── Caisse globale ─────────────────────────────── + UnionBalanceCard( + label: 'Caisse Globale Système', + amount: _formatAmount(stats?.totalContributionAmount ?? 0), + trend: stats != null && stats.totalContributionAmount > 0 + ? '${stats.monthlyGrowth >= 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}%' + : null, + isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0, + ), + const SizedBox(height: 12), - // ── Grille 3×2 KPIs ───────────────────────────── - GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 3, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - childAspectRatio: 2.0, - children: [ - UnionStatWidget( - label: 'Organisations', - value: '${stats?.totalOrganizations ?? 0}', - icon: Icons.business_rounded, - color: UnionFlowColors.unionGreen, - ), - UnionStatWidget( - label: 'Utilisateurs', - value: '${stats?.totalMembers ?? 0}', - icon: Icons.groups_rounded, - color: UnionFlowColors.gold, - trend: stats != null && stats.monthlyGrowth > 0 - ? '+${stats.monthlyGrowth.toStringAsFixed(0)}%' - : null, - ), - UnionStatWidget( - label: 'Événements', - value: '${stats?.totalEvents ?? 0}', - icon: Icons.event_rounded, - color: UnionFlowColors.info, - trend: stats != null && stats.upcomingEvents > 0 - ? '${stats.upcomingEvents} à venir' - : null, - ), - UnionStatWidget( - label: 'Cotisations', - value: '${stats?.totalContributions ?? 0}', - icon: Icons.payments_rounded, - color: UnionFlowColors.amber, - ), - UnionStatWidget( - label: 'Demandes', - value: '${stats?.pendingRequests ?? 0}', - icon: Icons.pending_actions_rounded, - color: (stats?.pendingRequests ?? 0) > 0 - ? UnionFlowColors.warning - : UnionFlowColors.success, - ), - UnionStatWidget( - label: 'Engagement', - value: stats != null - ? '${(stats.engagementRate * 100).toStringAsFixed(0)}%' - : '—', - icon: Icons.trending_up_rounded, - color: (stats?.engagementRate ?? 0) >= 0.7 - ? UnionFlowColors.success - : UnionFlowColors.warning, - ), + // ── KPIs 3×2 ──────────────────────────────────── + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + mainAxisSpacing: 6, + crossAxisSpacing: 6, + childAspectRatio: 2.0, + children: [ + UnionStatWidget( + label: 'Organisations', + value: '${stats?.totalOrganizations ?? 0}', + icon: Icons.business_rounded, + color: ModuleColors.organisations, + ), + UnionStatWidget( + label: 'Utilisateurs', + value: '${stats?.totalMembers ?? 0}', + icon: Icons.groups_rounded, + color: ModuleColors.membres, + trend: stats != null && stats.monthlyGrowth > 0 + ? '+${stats.monthlyGrowth.toStringAsFixed(0)}%' + : null, + ), + UnionStatWidget( + label: 'Événements', + value: '${stats?.totalEvents ?? 0}', + icon: Icons.event_rounded, + color: ModuleColors.evenements, + trend: stats != null && stats.upcomingEvents > 0 + ? '${stats.upcomingEvents} à venir' + : null, + ), + UnionStatWidget( + label: 'Cotisations', + value: '${stats?.totalContributions ?? 0}', + icon: Icons.payments_rounded, + color: ModuleColors.cotisations, + ), + UnionStatWidget( + label: 'Demandes', + value: '${stats?.pendingRequests ?? 0}', + icon: Icons.pending_actions_rounded, + color: (stats?.pendingRequests ?? 0) > 0 + ? ColorTokens.warning + : ColorTokens.success, + ), + UnionStatWidget( + label: 'Engagement', + value: stats != null + ? '${(stats.engagementRate * 100).toStringAsFixed(0)}%' + : '—', + icon: Icons.trending_up_rounded, + color: (stats?.engagementRate ?? 0) >= 0.7 + ? ColorTokens.success + : ColorTokens.warning, + ), + ], + ), + const SizedBox(height: 12), + + // ── Activité membres ───────────────────────────── + if (stats != null && stats.totalMembers > 0) ...[ + _MemberActivityCard(stats: stats), + const SizedBox(height: 12), ], - ), - const SizedBox(height: 12), - // ── Activité membres (pie compact) ─────────────── - if (stats != null && stats.totalMembers > 0) ...[ - _buildMemberActivityRow(stats), - const SizedBox(height: 12), - ], + // ── Répartition types d'org ────────────────────── + if (stats?.organizationTypeDistribution != null && + stats!.organizationTypeDistribution!.isNotEmpty) ...[ + _OrgTypeCard(stats: stats), + const SizedBox(height: 12), + ], - // ── Répartition types d'org ────────────────────── - if (stats?.organizationTypeDistribution != null && - stats!.organizationTypeDistribution!.isNotEmpty) ...[ - _buildOrgTypeRow(stats), - const SizedBox(height: 12), - ], - - // ── Événements à venir ─────────────────────────── - if (dashboardData != null && dashboardData.hasUpcomingEvents) ...[ + // ── Événements à venir ─────────────────────────── UFSectionHeader( 'Événements à venir', - trailing: '${dashboardData.upcomingEvents.length} programmés', + trailing: dashboardData != null && + dashboardData.hasUpcomingEvents + ? '${dashboardData.upcomingEvents.length} programmés' + : null, ), const SizedBox(height: 6), - ...dashboardData.upcomingEvents.take(3).map( - (e) => DashboardEventRow( - title: e.title, - date: e.formattedDate, - daysUntil: e.daysUntilEvent, - participants: - '${e.currentParticipants}/${e.maxParticipants}', - ), - ), + if (dashboardData != null && dashboardData.hasUpcomingEvents) + ...dashboardData.upcomingEvents.take(3).map( + (e) => DashboardEventRow( + title: e.title, + date: e.formattedDate, + daysUntil: e.daysUntilEvent, + participants: + '${e.currentParticipants}/${e.maxParticipants}', + ), + ) + else + _EmptyStateRow( + icon: Icons.event_available_outlined, + message: 'Aucun événement programmé', + ), const SizedBox(height: 12), - ], - // ── Activités récentes ─────────────────────────── - if (dashboardData != null && dashboardData.hasRecentActivity) ...[ + // ── Activités récentes ─────────────────────────── const UFSectionHeader('Activités récentes', trailing: 'Dernières actions'), const SizedBox(height: 6), - ...dashboardData.recentActivities.take(5).map( - (a) => DashboardActivityRow( - title: a.title, - description: a.description, - timeAgo: a.timeAgo, - icon: DashboardActivityRow.iconFor(a.type), - color: DashboardActivityRow.colorFor(a.type), - ), - ), + if (dashboardData != null && dashboardData.hasRecentActivity) + ...dashboardData.recentActivities.take(5).map( + (a) => DashboardActivityRow( + title: a.title, + description: a.description, + timeAgo: a.timeAgo, + icon: DashboardActivityRow.iconFor(a.type), + color: DashboardActivityRow.colorFor(a.type), + ), + ) + else + _EmptyStateRow( + icon: Icons.history_outlined, + message: 'Aucune activité récente', + ), const SizedBox(height: 12), - ], - // ── Actions rapides ────────────────────────────── - const UFSectionHeader('Actions'), - const SizedBox(height: 8), - GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 3, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - childAspectRatio: 2.8, - children: [ - UnionActionButton( - label: 'Organisations', - icon: Icons.business_rounded, - iconColor: UnionFlowColors.unionGreen, - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const OrganizationsPageWrapper())), - ), - UnionActionButton( - label: 'Utilisateurs', - icon: Icons.people_rounded, - iconColor: UnionFlowColors.gold, - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const UserManagementPage())), - ), - UnionActionButton( - label: 'Système', - icon: Icons.settings_rounded, - iconColor: UnionFlowColors.indigo, - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const SystemSettingsPage())), - ), - UnionActionButton( - label: 'Backup', - icon: Icons.backup_rounded, - iconColor: UnionFlowColors.warning, - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const BackupPage())), - ), - UnionActionButton( - label: 'Sécurité', - icon: Icons.security_rounded, - iconColor: UnionFlowColors.error, - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const SystemSettingsPage())), - ), - UnionActionButton( - label: 'Support', - icon: Icons.help_outline_rounded, - iconColor: UnionFlowColors.info, - onTap: () => Navigator.push(context, - MaterialPageRoute(builder: (_) => const HelpSupportPage())), - ), - ], - ), - ], + // ── Actions rapides ────────────────────────────── + const UFSectionHeader('Actions rapides'), + const SizedBox(height: 8), + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + mainAxisSpacing: 6, + crossAxisSpacing: 6, + childAspectRatio: 2.8, + children: [ + UnionActionButton( + label: 'Organisations', + icon: Icons.business_rounded, + iconColor: ModuleColors.organisations, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const OrganizationsPageWrapper())), + ), + UnionActionButton( + label: 'Utilisateurs', + icon: Icons.people_rounded, + iconColor: ModuleColors.membres, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const UserManagementPage())), + ), + UnionActionButton( + label: 'Système', + icon: Icons.settings_rounded, + iconColor: ModuleColors.systeme, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const SystemSettingsPage())), + ), + UnionActionButton( + label: 'Backup', + icon: Icons.backup_rounded, + iconColor: ModuleColors.backup, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const BackupPage())), + ), + // Bouton "Analytics" retiré : redondant avec + // l'item "Rapports & Analytics" du menu "Plus". + UnionActionButton( + label: 'Support', + icon: Icons.help_outline_rounded, + iconColor: ModuleColors.support, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const HelpSupportPage())), + ), + ], + ), + ], + ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ), ); } @@ -264,24 +318,45 @@ class SuperAdminDashboard extends StatelessWidget { // ─── AppBar ──────────────────────────────────────────────────────────────── PreferredSizeWidget _buildAppBar(BuildContext context) { + // L'AppBar a toujours un fond gradient sombre (navy) — les icônes de status bar + // sont donc toujours claires, indépendamment du thème de l'app. return AppBar( - backgroundColor: UnionFlowColors.surface, + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, elevation: 0, - scrolledUnderElevation: 1, - shadowColor: UnionFlowColors.border, + scrolledUnderElevation: 0, // Pas de surface tint sur le gradient + systemOverlayStyle: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, // icônes blanches sur gradient sombre + statusBarBrightness: Brightness.dark, // iOS : barre sombre + ), + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: ModuleColors.systemeGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), leading: Builder( builder: (ctx) => IconButton( - icon: const Icon(Icons.menu, size: 22, color: UnionFlowColors.textPrimary), + icon: const Icon(Icons.menu, size: 22, color: Colors.white), onPressed: () => Scaffold.of(ctx).openDrawer(), ), ), title: Row( children: [ + // Badge ROOT Container( width: 28, height: 28, decoration: BoxDecoration( - color: UnionFlowColors.error, + gradient: LinearGradient( + colors: ModuleColors.solidariteGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), borderRadius: BorderRadius.circular(6), ), alignment: Alignment.center, @@ -295,38 +370,81 @@ class SuperAdminDashboard extends StatelessWidget { ), ), const SizedBox(width: 8), - const Column( + Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: const [ Text( 'UnionFlow', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, - color: UnionFlowColors.textPrimary, + color: Colors.white, ), ), Text( 'Super Admin', - style: TextStyle( - fontSize: 10, - color: UnionFlowColors.textSecondary, - ), + style: TextStyle(fontSize: 10, color: Colors.white70), ), ], ), ], ), actions: [ + // Indicateur temps réel WebSocket + BlocBuilder( + builder: (context, state) { + final isLive = + state is DashboardLoaded || state is DashboardRefreshing; + return Tooltip( + message: isLive ? 'Temps réel actif' : 'En attente', + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 6), + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isLive + ? ColorTokens.successLight + : Colors.white30, + shape: BoxShape.circle, + boxShadow: isLive + ? [ + BoxShadow( + color: ColorTokens.successLight.withOpacity(0.6), + blurRadius: 6, + ) + ] + : null, + ), + ), + ), + ); + }, + ), UnionExportButton(onExport: (_) {}), const SizedBox(width: 6), ], ); } - // ─── Activité membres ───────────────────────────────────────────────────── + // ─── Helpers ────────────────────────────────────────────────────────────── - Widget _buildMemberActivityRow(DashboardStatsEntity stats) { + String _formatAmount(double amount) { + if (amount >= 1000000) return '${(amount / 1000000).toStringAsFixed(1)}M FCFA'; + if (amount >= 1000) return '${(amount / 1000).toStringAsFixed(0)}K FCFA'; + return '${amount.toStringAsFixed(0)} FCFA'; + } +} + +// ─── Widget : Activité membres (dark-mode aware) ─────────────────────────── + +class _MemberActivityCard extends StatelessWidget { + final DashboardStatsEntity stats; + const _MemberActivityCard({required this.stats}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; final total = stats.totalMembers; final active = stats.activeMembers; final inactive = total - active; @@ -335,27 +453,26 @@ class SuperAdminDashboard extends StatelessWidget { return Container( padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), decoration: BoxDecoration( - color: UnionFlowColors.surface, + color: scheme.surface, borderRadius: BorderRadius.circular(12), - border: Border.all(color: UnionFlowColors.border), + border: Border.all(color: scheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - const Text( + Text( 'Activité membres', style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: UnionFlowColors.textPrimary, - ), + fontSize: 12, + fontWeight: FontWeight.w700, + color: scheme.onSurface), ), const Spacer(), Text( '$total membres', - style: const TextStyle(fontSize: 10, color: UnionFlowColors.textTertiary), + style: TextStyle(fontSize: 10, color: scheme.onSurfaceVariant), ), ], ), @@ -365,29 +482,40 @@ class SuperAdminDashboard extends StatelessWidget { child: LinearProgressIndicator( value: pct, minHeight: 6, - backgroundColor: UnionFlowColors.border, - valueColor: const AlwaysStoppedAnimation(UnionFlowColors.success), + backgroundColor: scheme.outlineVariant, + valueColor: + const AlwaysStoppedAnimation(ColorTokens.success), ), ), const SizedBox(height: 8), Row( children: [ - _dot(UnionFlowColors.success), + _dot(ColorTokens.success), const SizedBox(width: 4), - const Text('Actifs', style: TextStyle(fontSize: 10, color: UnionFlowColors.textSecondary)), + Text('Actifs', + style: TextStyle( + fontSize: 10, color: scheme.onSurfaceVariant)), const SizedBox(width: 4), Text( '$active (${(pct * 100).toStringAsFixed(0)}%)', - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: scheme.onSurface), ), const Spacer(), - _dot(UnionFlowColors.border), + _dot(scheme.outlineVariant), const SizedBox(width: 4), - const Text('Inactifs', style: TextStyle(fontSize: 10, color: UnionFlowColors.textSecondary)), + Text('Inactifs', + style: TextStyle( + fontSize: 10, color: scheme.onSurfaceVariant)), const SizedBox(width: 4), Text( '$inactive (${total > 0 ? ((inactive / total) * 100).toStringAsFixed(0) : 0}%)', - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: scheme.onSurface), ), ], ), @@ -396,94 +524,137 @@ class SuperAdminDashboard extends StatelessWidget { ); } - // ─── Répartition types d'org ────────────────────────────────────────────── - - Widget _buildOrgTypeRow(DashboardStatsEntity stats) { - final dist = stats.organizationTypeDistribution!; - final total = dist.values.fold(0, (s, v) => s + v); - const colors = [ - UnionFlowColors.unionGreen, - UnionFlowColors.gold, - UnionFlowColors.info, - UnionFlowColors.warning, - UnionFlowColors.error, - ]; - final entries = dist.entries.toList(); - - return Container( - padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), - decoration: BoxDecoration( - color: UnionFlowColors.surface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: UnionFlowColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Text( - 'Types d\'organisation', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), - ), - const Spacer(), - Text('$total org.', style: const TextStyle(fontSize: 10, color: UnionFlowColors.textTertiary)), - ], - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Row( - children: entries.asMap().entries.map((e) { - final frac = total > 0 ? e.value.value / total : 0.0; - return Flexible( - flex: (frac * 100).round(), - child: Container(height: 6, color: colors[e.key % colors.length]), - ); - }).toList(), - ), - ), - const SizedBox(height: 8), - ...entries.asMap().entries.map((e) { - final color = colors[e.key % colors.length]; - final pct = total > 0 ? (e.value.value / total * 100).toStringAsFixed(0) : '0'; - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - _dot(color), - const SizedBox(width: 6), - Expanded( - child: Text( - e.value.key, - style: const TextStyle(fontSize: 10, color: UnionFlowColors.textSecondary), - overflow: TextOverflow.ellipsis, - ), - ), - Text( - '$pct% · ${e.value.value}', - style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary), - ), - ], - ), - ); - }), - ], - ), - ); - } - Widget _dot(Color color) => Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ); +} - // ─── Helpers ────────────────────────────────────────────────────────────── +// ─── Widget : Répartition types d'org (dark-mode aware) ─────────────────── - String _formatAmount(double amount) { - if (amount >= 1000000) return '${(amount / 1000000).toStringAsFixed(1)}M FCFA'; - if (amount >= 1000) return '${(amount / 1000).toStringAsFixed(0)}K FCFA'; - return '${amount.toStringAsFixed(0)} FCFA'; +class _OrgTypeCard extends StatelessWidget { + final DashboardStatsEntity stats; + const _OrgTypeCard({required this.stats}); + + static const _kColors = [ + ModuleColors.organisations, + ModuleColors.membres, + ModuleColors.evenements, + ModuleColors.cotisations, + ModuleColors.solidarite, + ]; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final dist = stats.organizationTypeDistribution!; + final total = dist.values.fold(0, (s, v) => s + v); + final entries = dist.entries.toList(); + + return Container( + padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), + decoration: BoxDecoration( + color: scheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: scheme.outlineVariant), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Types d\'organisation', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: scheme.onSurface), + ), + const Spacer(), + Text( + '$total org.', + style: TextStyle(fontSize: 10, color: scheme.onSurfaceVariant), + ), + ], + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Row( + children: entries.asMap().entries.map((e) { + final frac = total > 0 ? e.value.value / total : 0.0; + return Flexible( + flex: (frac * 100).round(), + child: Container( + height: 6, color: _kColors[e.key % _kColors.length]), + ); + }).toList(), + ), + ), + const SizedBox(height: 8), + ...entries.asMap().entries.map((e) { + final color = _kColors[e.key % _kColors.length]; + final pct = + total > 0 ? (e.value.value / total * 100).toStringAsFixed(0) : '0'; + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Expanded( + child: Text( + e.value.key, + style: TextStyle( + fontSize: 10, color: scheme.onSurfaceVariant), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '$pct% · ${e.value.value}', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: scheme.onSurface), + ), + ], + ), + ); + }), + ], + ), + ); + } +} + +// ─── Widget : État vide inline ───────────────────────────────────────────── + +class _EmptyStateRow extends StatelessWidget { + final IconData icon; + final String message; + const _EmptyStateRow({required this.icon, required this.message}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Icon(icon, size: 16, color: scheme.onSurfaceVariant), + const SizedBox(width: 8), + Text( + message, + style: TextStyle(fontSize: 12, color: scheme.onSurfaceVariant), + ), + ], + ), + ); } } diff --git a/lib/features/dashboard/presentation/pages/role_dashboards/visitor_dashboard.dart b/lib/features/dashboard/presentation/pages/role_dashboards/visitor_dashboard.dart index 4d9b106..9845569 100644 --- a/lib/features/dashboard/presentation/pages/role_dashboards/visitor_dashboard.dart +++ b/lib/features/dashboard/presentation/pages/role_dashboards/visitor_dashboard.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../shared/design_system/unionflow_design_v2.dart'; +import '../../../../../shared/design_system/tokens/app_colors.dart'; import '../../../../authentication/presentation/bloc/auth_bloc.dart'; /// Dashboard affiché pour un compte authentifié sans rôle métier actif. @@ -11,11 +12,17 @@ class VisitorDashboard extends StatelessWidget { @override Widget build(BuildContext context) { final authState = context.watch().state; - final email = authState is AuthAuthenticated ? authState.user.email : ''; + final email = authState is AuthAuthenticated ? authState.user.email : ''; final firstName = authState is AuthAuthenticated ? authState.user.firstName : ''; + final isDark = Theme.of(context).brightness == Brightness.dark; + final bgCard = isDark ? AppColors.surfaceDark : AppColors.surface; + final borderColor = isDark ? AppColors.borderDark : AppColors.border; + final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimary; + final textSecondary= isDark ? AppColors.textSecondaryDark: AppColors.textSecondary; + return Scaffold( - backgroundColor: UnionFlowColors.background, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: _buildAppBar(context), body: SingleChildScrollView( padding: const EdgeInsets.all(20), @@ -32,32 +39,20 @@ class VisitorDashboard extends StatelessWidget { color: UnionFlowColors.gold.withOpacity(0.12), shape: BoxShape.circle, ), - child: const Icon( - Icons.hourglass_empty_rounded, - size: 40, - color: UnionFlowColors.gold, - ), + child: const Icon(Icons.hourglass_empty_rounded, size: 40, color: UnionFlowColors.gold), ), const SizedBox(height: 20), // Titre Text( firstName.isNotEmpty ? 'Bonjour $firstName,' : 'Bienvenue,', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w800, - color: UnionFlowColors.textPrimary, - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: textPrimary), textAlign: TextAlign.center, ), const SizedBox(height: 8), - const Text( + Text( 'Votre compte est en attente d\'activation', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: UnionFlowColors.textPrimary, - ), + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textPrimary), textAlign: TextAlign.center, ), const SizedBox(height: 12), @@ -67,22 +62,16 @@ class VisitorDashboard extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( - color: UnionFlowColors.surface, + color: bgCard, borderRadius: BorderRadius.circular(8), - border: Border.all(color: UnionFlowColors.border), + border: Border.all(color: borderColor), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.email_outlined, size: 14, color: UnionFlowColors.textSecondary), + Icon(Icons.email_outlined, size: 14, color: textSecondary), const SizedBox(width: 6), - Text( - email, - style: const TextStyle( - fontSize: 13, - color: UnionFlowColors.textSecondary, - ), - ), + Text(email, style: TextStyle(fontSize: 13, color: textSecondary)), ], ), ), @@ -93,41 +82,38 @@ class VisitorDashboard extends StatelessWidget { width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: UnionFlowColors.surface, + color: bgCard, borderRadius: BorderRadius.circular(12), - border: const Border( - left: BorderSide(color: UnionFlowColors.gold, width: 3), - ), - boxShadow: UnionFlowColors.softShadow, + border: Border(left: BorderSide(color: UnionFlowColors.gold, width: 3)), + boxShadow: isDark ? null : UnionFlowColors.softShadow, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Que se passe-t-il ?', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: UnionFlowColors.textPrimary, - ), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: textPrimary), ), const SizedBox(height: 8), _buildStep( '1', 'Votre compte a été créé par l\'administrateur de votre organisation.', UnionFlowColors.unionGreen, + textSecondary, ), const SizedBox(height: 8), _buildStep( '2', 'Il doit être activé par un administrateur avant que vous puissiez accéder à la plateforme.', UnionFlowColors.gold, + textSecondary, ), const SizedBox(height: 8), _buildStep( '3', 'Une fois activé, reconnectez-vous pour accéder à votre espace.', UnionFlowColors.indigo, + textSecondary, ), ], ), @@ -138,9 +124,7 @@ class VisitorDashboard extends StatelessWidget { SizedBox( width: double.infinity, child: ElevatedButton.icon( - onPressed: () { - context.read().add(const AuthStatusChecked()); - }, + onPressed: () => context.read().add(const AuthStatusChecked()), icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text( 'Vérifier l\'état de mon compte', @@ -161,23 +145,20 @@ class VisitorDashboard extends StatelessWidget { SizedBox( width: double.infinity, child: OutlinedButton.icon( - onPressed: () { - context.read().add(const AuthLogoutRequested()); - }, - icon: const Icon(Icons.logout_rounded, size: 18), - label: const Text( + onPressed: () => context.read().add(const AuthLogoutRequested()), + icon: Icon(Icons.logout_rounded, size: 18, color: textSecondary), + label: Text( 'Se déconnecter', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: textSecondary), ), style: OutlinedButton.styleFrom( - foregroundColor: UnionFlowColors.textSecondary, - side: const BorderSide(color: UnionFlowColors.border), + foregroundColor: textSecondary, + side: BorderSide(color: borderColor), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ), - const SizedBox(height: 32), ], ), @@ -187,8 +168,10 @@ class VisitorDashboard extends StatelessWidget { PreferredSizeWidget _buildAppBar(BuildContext context) { return AppBar( - backgroundColor: UnionFlowColors.surface, + backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0, + scrolledUnderElevation: 1, + shadowColor: Theme.of(context).colorScheme.outline.withOpacity(0.2), automaticallyImplyLeading: false, title: Row( children: [ @@ -200,74 +183,48 @@ class VisitorDashboard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, - child: const Text( - 'U', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 18, - ), - ), + child: const Text('U', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 18)), ), const SizedBox(width: 12), - const Column( + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'UnionFlow', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: UnionFlowColors.textPrimary, - ), - ), - Text( - 'Compte en attente', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w400, - color: UnionFlowColors.textSecondary, - ), - ), + Text('UnionFlow', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurface, + )), + Text('Compte en attente', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + )), ], ), ], ), - iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary), + iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface), ); } - Widget _buildStep(String number, String text, Color color) { + Widget _buildStep(String number, String text, Color badgeColor, Color textColor) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 22, height: 22, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), + decoration: BoxDecoration(color: badgeColor, shape: BoxShape.circle), alignment: Alignment.center, - child: Text( - number, - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w800, - color: Colors.white, - ), - ), + child: Text(number, + style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: Colors.white)), ), const SizedBox(width: 10), Expanded( - child: Text( - text, - style: const TextStyle( - fontSize: 12, - height: 1.5, - color: UnionFlowColors.textSecondary, - ), - ), + child: Text(text, style: TextStyle(fontSize: 12, height: 1.5, color: textColor)), ), ], );