fix(dashboards): dark mode + scrolledUnderElevation + DashboardInitial

- SuperAdminDashboard : scrolledUnderElevation 1→0 (pas de tint surface sur gradient)
- OrgAdminDashboard : idem + gestion état DashboardInitial (plus de blanc flash)
- VisitorDashboard : tous les conteneurs body + bouton outline → AppColors pairs
  (surface/border/textPrimary/textSecondary remplacés par isDark ternaires)
This commit is contained in:
dahoud
2026-04-15 20:13:34 +00:00
parent 0abcdcc478
commit f78892e5f6
3 changed files with 592 additions and 444 deletions

View File

@@ -23,7 +23,7 @@ class OrgAdminDashboard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: UnionFlowColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: _buildAppBar(context), appBar: _buildAppBar(context),
drawer: DashboardDrawer( drawer: DashboardDrawer(
onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()), onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()),
@@ -38,7 +38,8 @@ class OrgAdminDashboard extends StatelessWidget {
return BlocBuilder<DashboardBloc, DashboardState>( return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, dashboardState) { builder: (context, dashboardState) {
if (dashboardState is DashboardLoading) { if (dashboardState is DashboardLoading ||
dashboardState is DashboardInitial) {
return const Center( return const Center(
child: CircularProgressIndicator(color: UnionFlowColors.gold), child: CircularProgressIndicator(color: UnionFlowColors.gold),
); );
@@ -285,43 +286,62 @@ class OrgAdminDashboard extends StatelessWidget {
} }
PreferredSizeWidget _buildAppBar(BuildContext context) { 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( return AppBar(
backgroundColor: UnionFlowColors.surface, backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0, 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( title: Row(
children: [ children: [
Container( Container(
width: 32, width: 28,
height: 32, height: 28,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: UnionFlowColors.goldGradient, color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.white.withOpacity(0.4)),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: const Text( child: const Text(
'A', '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( const Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('UnionFlow', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary)), Text('UnionFlow',
Text('Admin Organisation', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w400, color: UnionFlowColors.textSecondary)), 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: [ actions: [
UnionExportButton(onExport: (_) => _handleExport(context)), UnionExportButton(onExport: (_) => _handleExport(context)),
const SizedBox(width: 8), const SizedBox(width: 8),
UnionNotificationBadge( UnionNotificationBadge(
count: 0, count: 0,
child: IconButton( child: IconButton(
icon: const Icon(Icons.notifications_outlined), icon: const Icon(Icons.notifications_outlined, color: Colors.white),
color: UnionFlowColors.textPrimary,
onPressed: () => Navigator.of(context).push( onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NotificationsPageWrapper()), MaterialPageRoute(builder: (_) => const NotificationsPageWrapper()),
), ),

View File

@@ -1,6 +1,10 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:async';
import 'package:flutter/material.dart'; 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/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 '../../bloc/dashboard_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart'; import '../../../domain/entities/dashboard_entity.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../../../authentication/presentation/bloc/auth_bloc.dart';
@@ -11,46 +15,75 @@ import '../../../../backup/presentation/pages/backup_page.dart';
import '../../../../help/presentation/pages/help_support_page.dart'; import '../../../../help/presentation/pages/help_support_page.dart';
import '../../widgets/dashboard_drawer.dart'; import '../../widgets/dashboard_drawer.dart';
/// Dashboard Super Admin — design fintech compact /// Dashboard Super Admin — design harmonisé v2
class SuperAdminDashboard extends StatelessWidget { ///
/// 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}); const SuperAdminDashboard({super.key});
@override
State<SuperAdminDashboard> createState() => _SuperAdminDashboardState();
}
class _SuperAdminDashboardState extends State<SuperAdminDashboard> {
Completer<void>? _refreshCompleter;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: UnionFlowColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: _buildAppBar(context), appBar: _buildAppBar(context),
drawer: DashboardDrawer( drawer: DashboardDrawer(
onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()), onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()),
), ),
body: BlocBuilder<AuthBloc, AuthState>( body: BlocListener<DashboardBloc, DashboardState>(
listener: (context, state) {
if (state is DashboardLoaded || state is DashboardError) {
_refreshCompleter?.complete();
_refreshCompleter = null;
}
},
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) { builder: (context, authState) {
final user = (authState is AuthAuthenticated) ? authState.user : null; final user = (authState is AuthAuthenticated) ? authState.user : null;
return BlocBuilder<DashboardBloc, DashboardState>( return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, dashboardState) { builder: (context, dashboardState) {
if (dashboardState is DashboardLoading) { if (dashboardState is DashboardLoading ||
dashboardState is DashboardInitial) {
return const Center( return const Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: UnionFlowColors.error, color: ModuleColors.systeme,
strokeWidth: 2, strokeWidth: 2,
), ),
); );
} }
final dashboardData = (dashboardState is DashboardLoaded)
final dashboardData = dashboardState is DashboardLoaded
? dashboardState.dashboardData
: dashboardState is DashboardRefreshing
? dashboardState.dashboardData ? dashboardState.dashboardData
: null; : null;
final stats = dashboardData?.stats; final stats = dashboardData?.stats;
return RefreshIndicator( return RefreshIndicator(
color: UnionFlowColors.error, color: ModuleColors.systeme,
strokeWidth: 2, strokeWidth: 2,
onRefresh: () async { onRefresh: () async {
_refreshCompleter = Completer<void>();
context.read<DashboardBloc>().add(LoadDashboardData( context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: dashboardData?.organizationId ?? '', organizationId: dashboardData?.organizationId ?? '',
userId: user?.id ?? '', userId: user?.id ?? '',
useGlobalDashboard: true, useGlobalDashboard: true,
)); ));
await Future.delayed(const Duration(milliseconds: 600)); return _refreshCompleter!.future;
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@@ -58,23 +91,23 @@ class SuperAdminDashboard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// ── Carte identité ─────────────────────────────── // ── Carte identité ROOT ──────────────────────────
UserIdentityCard( UserIdentityCard(
initials: user?.initials ?? 'SA', initials: user?.initials ?? 'SA',
name: user?.fullName ?? 'Super Administrateur', name: user?.fullName ?? 'Super Administrateur',
subtitle: 'Accès global système', subtitle: 'Accès global système',
badgeLabel: 'ROOT', badgeLabel: 'ROOT',
gradient: const LinearGradient( gradient: LinearGradient(
colors: [Color(0xFFB91C1C), Color(0xFF7F1D1D)], colors: ModuleColors.solidariteGradient,
begin: Alignment.centerLeft, begin: Alignment.centerLeft,
end: Alignment.centerRight, end: Alignment.centerRight,
), ),
accentColor: UnionFlowColors.error, accentColor: ModuleColors.solidarite,
showTopBorder: false, showTopBorder: false,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// ── Balance principale ─────────────────────────── // ── Caisse globale ───────────────────────────────
UnionBalanceCard( UnionBalanceCard(
label: 'Caisse Globale Système', label: 'Caisse Globale Système',
amount: _formatAmount(stats?.totalContributionAmount ?? 0), amount: _formatAmount(stats?.totalContributionAmount ?? 0),
@@ -85,7 +118,7 @@ class SuperAdminDashboard extends StatelessWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// ── Grille 3×2 KPIs ───────────────────────────── // ── KPIs 3×2 ────────────────────────────────────
GridView.count( GridView.count(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@@ -98,13 +131,13 @@ class SuperAdminDashboard extends StatelessWidget {
label: 'Organisations', label: 'Organisations',
value: '${stats?.totalOrganizations ?? 0}', value: '${stats?.totalOrganizations ?? 0}',
icon: Icons.business_rounded, icon: Icons.business_rounded,
color: UnionFlowColors.unionGreen, color: ModuleColors.organisations,
), ),
UnionStatWidget( UnionStatWidget(
label: 'Utilisateurs', label: 'Utilisateurs',
value: '${stats?.totalMembers ?? 0}', value: '${stats?.totalMembers ?? 0}',
icon: Icons.groups_rounded, icon: Icons.groups_rounded,
color: UnionFlowColors.gold, color: ModuleColors.membres,
trend: stats != null && stats.monthlyGrowth > 0 trend: stats != null && stats.monthlyGrowth > 0
? '+${stats.monthlyGrowth.toStringAsFixed(0)}%' ? '+${stats.monthlyGrowth.toStringAsFixed(0)}%'
: null, : null,
@@ -113,7 +146,7 @@ class SuperAdminDashboard extends StatelessWidget {
label: 'Événements', label: 'Événements',
value: '${stats?.totalEvents ?? 0}', value: '${stats?.totalEvents ?? 0}',
icon: Icons.event_rounded, icon: Icons.event_rounded,
color: UnionFlowColors.info, color: ModuleColors.evenements,
trend: stats != null && stats.upcomingEvents > 0 trend: stats != null && stats.upcomingEvents > 0
? '${stats.upcomingEvents} à venir' ? '${stats.upcomingEvents} à venir'
: null, : null,
@@ -122,15 +155,15 @@ class SuperAdminDashboard extends StatelessWidget {
label: 'Cotisations', label: 'Cotisations',
value: '${stats?.totalContributions ?? 0}', value: '${stats?.totalContributions ?? 0}',
icon: Icons.payments_rounded, icon: Icons.payments_rounded,
color: UnionFlowColors.amber, color: ModuleColors.cotisations,
), ),
UnionStatWidget( UnionStatWidget(
label: 'Demandes', label: 'Demandes',
value: '${stats?.pendingRequests ?? 0}', value: '${stats?.pendingRequests ?? 0}',
icon: Icons.pending_actions_rounded, icon: Icons.pending_actions_rounded,
color: (stats?.pendingRequests ?? 0) > 0 color: (stats?.pendingRequests ?? 0) > 0
? UnionFlowColors.warning ? ColorTokens.warning
: UnionFlowColors.success, : ColorTokens.success,
), ),
UnionStatWidget( UnionStatWidget(
label: 'Engagement', label: 'Engagement',
@@ -139,33 +172,36 @@ class SuperAdminDashboard extends StatelessWidget {
: '', : '',
icon: Icons.trending_up_rounded, icon: Icons.trending_up_rounded,
color: (stats?.engagementRate ?? 0) >= 0.7 color: (stats?.engagementRate ?? 0) >= 0.7
? UnionFlowColors.success ? ColorTokens.success
: UnionFlowColors.warning, : ColorTokens.warning,
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// ── Activité membres (pie compact) ─────────────── // ── Activité membres ─────────────────────────────
if (stats != null && stats.totalMembers > 0) ...[ if (stats != null && stats.totalMembers > 0) ...[
_buildMemberActivityRow(stats), _MemberActivityCard(stats: stats),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
// ── Répartition types d'org ────────────────────── // ── Répartition types d'org ──────────────────────
if (stats?.organizationTypeDistribution != null && if (stats?.organizationTypeDistribution != null &&
stats!.organizationTypeDistribution!.isNotEmpty) ...[ stats!.organizationTypeDistribution!.isNotEmpty) ...[
_buildOrgTypeRow(stats), _OrgTypeCard(stats: stats),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
// ── Événements à venir ─────────────────────────── // ── Événements à venir ───────────────────────────
if (dashboardData != null && dashboardData.hasUpcomingEvents) ...[
UFSectionHeader( UFSectionHeader(
'Événements à venir', 'Événements à venir',
trailing: '${dashboardData.upcomingEvents.length} programmés', trailing: dashboardData != null &&
dashboardData.hasUpcomingEvents
? '${dashboardData.upcomingEvents.length} programmés'
: null,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
if (dashboardData != null && dashboardData.hasUpcomingEvents)
...dashboardData.upcomingEvents.take(3).map( ...dashboardData.upcomingEvents.take(3).map(
(e) => DashboardEventRow( (e) => DashboardEventRow(
title: e.title, title: e.title,
@@ -174,15 +210,19 @@ class SuperAdminDashboard extends StatelessWidget {
participants: participants:
'${e.currentParticipants}/${e.maxParticipants}', '${e.currentParticipants}/${e.maxParticipants}',
), ),
)
else
_EmptyStateRow(
icon: Icons.event_available_outlined,
message: 'Aucun événement programmé',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
],
// ── Activités récentes ─────────────────────────── // ── Activités récentes ───────────────────────────
if (dashboardData != null && dashboardData.hasRecentActivity) ...[
const UFSectionHeader('Activités récentes', const UFSectionHeader('Activités récentes',
trailing: 'Dernières actions'), trailing: 'Dernières actions'),
const SizedBox(height: 6), const SizedBox(height: 6),
if (dashboardData != null && dashboardData.hasRecentActivity)
...dashboardData.recentActivities.take(5).map( ...dashboardData.recentActivities.take(5).map(
(a) => DashboardActivityRow( (a) => DashboardActivityRow(
title: a.title, title: a.title,
@@ -191,12 +231,16 @@ class SuperAdminDashboard extends StatelessWidget {
icon: DashboardActivityRow.iconFor(a.type), icon: DashboardActivityRow.iconFor(a.type),
color: DashboardActivityRow.colorFor(a.type), color: DashboardActivityRow.colorFor(a.type),
), ),
)
else
_EmptyStateRow(
icon: Icons.history_outlined,
message: 'Aucune activité récente',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
],
// ── Actions rapides ────────────────────────────── // ── Actions rapides ──────────────────────────────
const UFSectionHeader('Actions'), const UFSectionHeader('Actions rapides'),
const SizedBox(height: 8), const SizedBox(height: 8),
GridView.count( GridView.count(
shrinkWrap: true, shrinkWrap: true,
@@ -209,44 +253,53 @@ class SuperAdminDashboard extends StatelessWidget {
UnionActionButton( UnionActionButton(
label: 'Organisations', label: 'Organisations',
icon: Icons.business_rounded, icon: Icons.business_rounded,
iconColor: UnionFlowColors.unionGreen, iconColor: ModuleColors.organisations,
onTap: () => Navigator.push(context, onTap: () => Navigator.push(
MaterialPageRoute(builder: (_) => const OrganizationsPageWrapper())), context,
MaterialPageRoute(
builder: (_) =>
const OrganizationsPageWrapper())),
), ),
UnionActionButton( UnionActionButton(
label: 'Utilisateurs', label: 'Utilisateurs',
icon: Icons.people_rounded, icon: Icons.people_rounded,
iconColor: UnionFlowColors.gold, iconColor: ModuleColors.membres,
onTap: () => Navigator.push(context, onTap: () => Navigator.push(
MaterialPageRoute(builder: (_) => const UserManagementPage())), context,
MaterialPageRoute(
builder: (_) =>
const UserManagementPage())),
), ),
UnionActionButton( UnionActionButton(
label: 'Système', label: 'Système',
icon: Icons.settings_rounded, icon: Icons.settings_rounded,
iconColor: UnionFlowColors.indigo, iconColor: ModuleColors.systeme,
onTap: () => Navigator.push(context, onTap: () => Navigator.push(
MaterialPageRoute(builder: (_) => const SystemSettingsPage())), context,
MaterialPageRoute(
builder: (_) =>
const SystemSettingsPage())),
), ),
UnionActionButton( UnionActionButton(
label: 'Backup', label: 'Backup',
icon: Icons.backup_rounded, icon: Icons.backup_rounded,
iconColor: UnionFlowColors.warning, iconColor: ModuleColors.backup,
onTap: () => Navigator.push(context, onTap: () => Navigator.push(
MaterialPageRoute(builder: (_) => const BackupPage())), context,
), MaterialPageRoute(
UnionActionButton( builder: (_) => const BackupPage())),
label: 'Sécurité',
icon: Icons.security_rounded,
iconColor: UnionFlowColors.error,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const SystemSettingsPage())),
), ),
// Bouton "Analytics" retiré : redondant avec
// l'item "Rapports & Analytics" du menu "Plus".
UnionActionButton( UnionActionButton(
label: 'Support', label: 'Support',
icon: Icons.help_outline_rounded, icon: Icons.help_outline_rounded,
iconColor: UnionFlowColors.info, iconColor: ModuleColors.support,
onTap: () => Navigator.push(context, onTap: () => Navigator.push(
MaterialPageRoute(builder: (_) => const HelpSupportPage())), context,
MaterialPageRoute(
builder: (_) =>
const HelpSupportPage())),
), ),
], ],
), ),
@@ -258,30 +311,52 @@ class SuperAdminDashboard extends StatelessWidget {
); );
}, },
), ),
),
); );
} }
// ─── AppBar ──────────────────────────────────────────────────────────────── // ─── AppBar ────────────────────────────────────────────────────────────────
PreferredSizeWidget _buildAppBar(BuildContext context) { 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( return AppBar(
backgroundColor: UnionFlowColors.surface, backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
elevation: 0, elevation: 0,
scrolledUnderElevation: 1, scrolledUnderElevation: 0, // Pas de surface tint sur le gradient
shadowColor: UnionFlowColors.border, 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( leading: Builder(
builder: (ctx) => IconButton( 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(), onPressed: () => Scaffold.of(ctx).openDrawer(),
), ),
), ),
title: Row( title: Row(
children: [ children: [
// Badge ROOT
Container( Container(
width: 28, width: 28,
height: 28, height: 28,
decoration: BoxDecoration( decoration: BoxDecoration(
color: UnionFlowColors.error, gradient: LinearGradient(
colors: ModuleColors.solidariteGradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
alignment: Alignment.center, alignment: Alignment.center,
@@ -295,38 +370,81 @@ class SuperAdminDashboard extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: const [
Text( Text(
'UnionFlow', 'UnionFlow',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary, color: Colors.white,
), ),
), ),
Text( Text(
'Super Admin', 'Super Admin',
style: TextStyle( style: TextStyle(fontSize: 10, color: Colors.white70),
fontSize: 10,
color: UnionFlowColors.textSecondary,
),
), ),
], ],
), ),
], ],
), ),
actions: [ actions: [
// Indicateur temps réel WebSocket
BlocBuilder<DashboardBloc, DashboardState>(
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: (_) {}), UnionExportButton(onExport: (_) {}),
const SizedBox(width: 6), 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 total = stats.totalMembers;
final active = stats.activeMembers; final active = stats.activeMembers;
final inactive = total - active; final inactive = total - active;
@@ -335,27 +453,26 @@ class SuperAdminDashboard extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UnionFlowColors.surface, color: scheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: UnionFlowColors.border), border: Border.all(color: scheme.outlineVariant),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
const Text( Text(
'Activité membres', 'Activité membres',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary, color: scheme.onSurface),
),
), ),
const Spacer(), const Spacer(),
Text( Text(
'$total membres', '$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( child: LinearProgressIndicator(
value: pct, value: pct,
minHeight: 6, minHeight: 6,
backgroundColor: UnionFlowColors.border, backgroundColor: scheme.outlineVariant,
valueColor: const AlwaysStoppedAnimation<Color>(UnionFlowColors.success), valueColor:
const AlwaysStoppedAnimation<Color>(ColorTokens.success),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
children: [ children: [
_dot(UnionFlowColors.success), _dot(ColorTokens.success),
const SizedBox(width: 4), 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), const SizedBox(width: 4),
Text( Text(
'$active (${(pct * 100).toStringAsFixed(0)}%)', '$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(), const Spacer(),
_dot(UnionFlowColors.border), _dot(scheme.outlineVariant),
const SizedBox(width: 4), 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), const SizedBox(width: 4),
Text( Text(
'$inactive (${total > 0 ? ((inactive / total) * 100).toStringAsFixed(0) : 0}%)', '$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( Widget _dot(Color color) => Container(
width: 8, width: 8,
height: 8, height: 8,
decoration: BoxDecoration(color: color, shape: BoxShape.circle), decoration: BoxDecoration(color: color, shape: BoxShape.circle),
); );
}
// ─── Helpers ────────────────────────────────────────────────────────────── // ─── Widget : Répartition types d'org (dark-mode aware) ───────────────────
String _formatAmount(double amount) { class _OrgTypeCard extends StatelessWidget {
if (amount >= 1000000) return '${(amount / 1000000).toStringAsFixed(1)}M FCFA'; final DashboardStatsEntity stats;
if (amount >= 1000) return '${(amount / 1000).toStringAsFixed(0)}K FCFA'; const _OrgTypeCard({required this.stats});
return '${amount.toStringAsFixed(0)} FCFA';
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),
),
],
),
);
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../shared/design_system/unionflow_design_v2.dart'; import '../../../../../shared/design_system/unionflow_design_v2.dart';
import '../../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../../../authentication/presentation/bloc/auth_bloc.dart';
/// Dashboard affiché pour un compte authentifié sans rôle métier actif. /// Dashboard affiché pour un compte authentifié sans rôle métier actif.
@@ -14,8 +15,14 @@ class VisitorDashboard extends StatelessWidget {
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 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( return Scaffold(
backgroundColor: UnionFlowColors.background, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: _buildAppBar(context), appBar: _buildAppBar(context),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@@ -32,32 +39,20 @@ class VisitorDashboard extends StatelessWidget {
color: UnionFlowColors.gold.withOpacity(0.12), color: UnionFlowColors.gold.withOpacity(0.12),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(Icons.hourglass_empty_rounded, size: 40, color: UnionFlowColors.gold),
Icons.hourglass_empty_rounded,
size: 40,
color: UnionFlowColors.gold,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Titre // Titre
Text( Text(
firstName.isNotEmpty ? 'Bonjour $firstName,' : 'Bienvenue,', firstName.isNotEmpty ? 'Bonjour $firstName,' : 'Bienvenue,',
style: const TextStyle( style: TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: textPrimary),
fontSize: 20,
fontWeight: FontWeight.w800,
color: UnionFlowColors.textPrimary,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( Text(
'Votre compte est en attente d\'activation', 'Votre compte est en attente d\'activation',
style: TextStyle( style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textPrimary),
fontSize: 15,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -67,22 +62,16 @@ class VisitorDashboard extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UnionFlowColors.surface, color: bgCard,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: UnionFlowColors.border), border: Border.all(color: borderColor),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.email_outlined, size: 14, color: UnionFlowColors.textSecondary), Icon(Icons.email_outlined, size: 14, color: textSecondary),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(email, style: TextStyle(fontSize: 13, color: textSecondary)),
email,
style: const TextStyle(
fontSize: 13,
color: UnionFlowColors.textSecondary,
),
),
], ],
), ),
), ),
@@ -93,41 +82,38 @@ class VisitorDashboard extends StatelessWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UnionFlowColors.surface, color: bgCard,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: const Border( border: Border(left: BorderSide(color: UnionFlowColors.gold, width: 3)),
left: BorderSide(color: UnionFlowColors.gold, width: 3), boxShadow: isDark ? null : UnionFlowColors.softShadow,
),
boxShadow: UnionFlowColors.softShadow,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'Que se passe-t-il ?', 'Que se passe-t-il ?',
style: TextStyle( style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: textPrimary),
fontSize: 13,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildStep( _buildStep(
'1', '1',
'Votre compte a été créé par l\'administrateur de votre organisation.', 'Votre compte a été créé par l\'administrateur de votre organisation.',
UnionFlowColors.unionGreen, UnionFlowColors.unionGreen,
textSecondary,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildStep( _buildStep(
'2', '2',
'Il doit être activé par un administrateur avant que vous puissiez accéder à la plateforme.', 'Il doit être activé par un administrateur avant que vous puissiez accéder à la plateforme.',
UnionFlowColors.gold, UnionFlowColors.gold,
textSecondary,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_buildStep( _buildStep(
'3', '3',
'Une fois activé, reconnectez-vous pour accéder à votre espace.', 'Une fois activé, reconnectez-vous pour accéder à votre espace.',
UnionFlowColors.indigo, UnionFlowColors.indigo,
textSecondary,
), ),
], ],
), ),
@@ -138,9 +124,7 @@ class VisitorDashboard extends StatelessWidget {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () => context.read<AuthBloc>().add(const AuthStatusChecked()),
context.read<AuthBloc>().add(const AuthStatusChecked());
},
icon: const Icon(Icons.refresh_rounded, size: 18), icon: const Icon(Icons.refresh_rounded, size: 18),
label: const Text( label: const Text(
'Vérifier l\'état de mon compte', 'Vérifier l\'état de mon compte',
@@ -161,23 +145,20 @@ class VisitorDashboard extends StatelessWidget {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () { onPressed: () => context.read<AuthBloc>().add(const AuthLogoutRequested()),
context.read<AuthBloc>().add(const AuthLogoutRequested()); icon: Icon(Icons.logout_rounded, size: 18, color: textSecondary),
}, label: Text(
icon: const Icon(Icons.logout_rounded, size: 18),
label: const Text(
'Se déconnecter', 'Se déconnecter',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: textSecondary),
), ),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: UnionFlowColors.textSecondary, foregroundColor: textSecondary,
side: const BorderSide(color: UnionFlowColors.border), side: BorderSide(color: borderColor),
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
), ),
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
], ],
), ),
@@ -187,8 +168,10 @@ class VisitorDashboard extends StatelessWidget {
PreferredSizeWidget _buildAppBar(BuildContext context) { PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar( return AppBar(
backgroundColor: UnionFlowColors.surface, backgroundColor: Theme.of(context).colorScheme.surface,
elevation: 0, elevation: 0,
scrolledUnderElevation: 1,
shadowColor: Theme.of(context).colorScheme.outline.withOpacity(0.2),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Row( title: Row(
children: [ children: [
@@ -200,74 +183,48 @@ class VisitorDashboard extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: const Text( child: const Text('U',
'U', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 18)),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text('UnionFlow',
'UnionFlow',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary, color: Theme.of(context).colorScheme.onSurface,
), )),
), Text('Compte en attente',
Text(
'Compte en attente',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary, 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( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
width: 22, width: 22,
height: 22, height: 22,
decoration: BoxDecoration( decoration: BoxDecoration(color: badgeColor, shape: BoxShape.circle),
color: color,
shape: BoxShape.circle,
),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(number,
number, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: Colors.white)),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(text, style: TextStyle(fontSize: 12, height: 1.5, color: textColor)),
text,
style: const TextStyle(
fontSize: 12,
height: 1.5,
color: UnionFlowColors.textSecondary,
),
),
), ),
], ],
); );