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
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: _buildAppBar(context),
drawer: DashboardDrawer(
onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()),
@@ -38,7 +38,8 @@ class OrgAdminDashboard extends StatelessWidget {
return BlocBuilder<DashboardBloc, DashboardState>(
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()),
),

View File

@@ -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,46 +15,75 @@ 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<SuperAdminDashboard> createState() => _SuperAdminDashboardState();
}
class _SuperAdminDashboardState extends State<SuperAdminDashboard> {
Completer<void>? _refreshCompleter;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: _buildAppBar(context),
drawer: DashboardDrawer(
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) {
final user = (authState is AuthAuthenticated) ? authState.user : null;
return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, dashboardState) {
if (dashboardState is DashboardLoading) {
if (dashboardState is DashboardLoading ||
dashboardState is DashboardInitial) {
return const Center(
child: CircularProgressIndicator(
color: UnionFlowColors.error,
color: ModuleColors.systeme,
strokeWidth: 2,
),
);
}
final dashboardData = (dashboardState is DashboardLoaded)
final dashboardData = dashboardState is DashboardLoaded
? dashboardState.dashboardData
: dashboardState is DashboardRefreshing
? dashboardState.dashboardData
: null;
final stats = dashboardData?.stats;
return RefreshIndicator(
color: UnionFlowColors.error,
color: ModuleColors.systeme,
strokeWidth: 2,
onRefresh: () async {
_refreshCompleter = Completer<void>();
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: dashboardData?.organizationId ?? '',
userId: user?.id ?? '',
useGlobalDashboard: true,
));
await Future.delayed(const Duration(milliseconds: 600));
return _refreshCompleter!.future;
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -58,23 +91,23 @@ class SuperAdminDashboard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Carte identité ───────────────────────────────
// ── Carte identité ROOT ──────────────────────────
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)],
gradient: LinearGradient(
colors: ModuleColors.solidariteGradient,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
accentColor: UnionFlowColors.error,
accentColor: ModuleColors.solidarite,
showTopBorder: false,
),
const SizedBox(height: 12),
// ── Balance principale ───────────────────────────
// ── Caisse globale ───────────────────────────────
UnionBalanceCard(
label: 'Caisse Globale Système',
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
@@ -85,7 +118,7 @@ class SuperAdminDashboard extends StatelessWidget {
),
const SizedBox(height: 12),
// ── Grille 3×2 KPIs ─────────────────────────────
// ── KPIs 3×2 ────────────────────────────────────
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -98,13 +131,13 @@ class SuperAdminDashboard extends StatelessWidget {
label: 'Organisations',
value: '${stats?.totalOrganizations ?? 0}',
icon: Icons.business_rounded,
color: UnionFlowColors.unionGreen,
color: ModuleColors.organisations,
),
UnionStatWidget(
label: 'Utilisateurs',
value: '${stats?.totalMembers ?? 0}',
icon: Icons.groups_rounded,
color: UnionFlowColors.gold,
color: ModuleColors.membres,
trend: stats != null && stats.monthlyGrowth > 0
? '+${stats.monthlyGrowth.toStringAsFixed(0)}%'
: null,
@@ -113,7 +146,7 @@ class SuperAdminDashboard extends StatelessWidget {
label: 'Événements',
value: '${stats?.totalEvents ?? 0}',
icon: Icons.event_rounded,
color: UnionFlowColors.info,
color: ModuleColors.evenements,
trend: stats != null && stats.upcomingEvents > 0
? '${stats.upcomingEvents} à venir'
: null,
@@ -122,15 +155,15 @@ class SuperAdminDashboard extends StatelessWidget {
label: 'Cotisations',
value: '${stats?.totalContributions ?? 0}',
icon: Icons.payments_rounded,
color: UnionFlowColors.amber,
color: ModuleColors.cotisations,
),
UnionStatWidget(
label: 'Demandes',
value: '${stats?.pendingRequests ?? 0}',
icon: Icons.pending_actions_rounded,
color: (stats?.pendingRequests ?? 0) > 0
? UnionFlowColors.warning
: UnionFlowColors.success,
? ColorTokens.warning
: ColorTokens.success,
),
UnionStatWidget(
label: 'Engagement',
@@ -139,33 +172,36 @@ class SuperAdminDashboard extends StatelessWidget {
: '',
icon: Icons.trending_up_rounded,
color: (stats?.engagementRate ?? 0) >= 0.7
? UnionFlowColors.success
: UnionFlowColors.warning,
? ColorTokens.success
: ColorTokens.warning,
),
],
),
const SizedBox(height: 12),
// ── Activité membres (pie compact) ───────────────
// ── Activité membres ─────────────────────────────
if (stats != null && stats.totalMembers > 0) ...[
_buildMemberActivityRow(stats),
_MemberActivityCard(stats: stats),
const SizedBox(height: 12),
],
// ── Répartition types d'org ──────────────────────
if (stats?.organizationTypeDistribution != null &&
stats!.organizationTypeDistribution!.isNotEmpty) ...[
_buildOrgTypeRow(stats),
_OrgTypeCard(stats: stats),
const SizedBox(height: 12),
],
// ── Événements à venir ───────────────────────────
if (dashboardData != null && dashboardData.hasUpcomingEvents) ...[
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),
if (dashboardData != null && dashboardData.hasUpcomingEvents)
...dashboardData.upcomingEvents.take(3).map(
(e) => DashboardEventRow(
title: e.title,
@@ -174,15 +210,19 @@ class SuperAdminDashboard extends StatelessWidget {
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) ...[
const UFSectionHeader('Activités récentes',
trailing: 'Dernières actions'),
const SizedBox(height: 6),
if (dashboardData != null && dashboardData.hasRecentActivity)
...dashboardData.recentActivities.take(5).map(
(a) => DashboardActivityRow(
title: a.title,
@@ -191,12 +231,16 @@ class SuperAdminDashboard extends StatelessWidget {
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 UFSectionHeader('Actions rapides'),
const SizedBox(height: 8),
GridView.count(
shrinkWrap: true,
@@ -209,44 +253,53 @@ class SuperAdminDashboard extends StatelessWidget {
UnionActionButton(
label: 'Organisations',
icon: Icons.business_rounded,
iconColor: UnionFlowColors.unionGreen,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const OrganizationsPageWrapper())),
iconColor: ModuleColors.organisations,
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())),
iconColor: ModuleColors.membres,
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())),
iconColor: ModuleColors.systeme,
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())),
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: UnionFlowColors.info,
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const HelpSupportPage())),
iconColor: ModuleColors.support,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
const HelpSupportPage())),
),
],
),
@@ -258,30 +311,52 @@ 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<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: (_) {}),
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,
),
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<Color>(UnionFlowColors.success),
backgroundColor: scheme.outlineVariant,
valueColor:
const AlwaysStoppedAnimation<Color>(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),
),
],
),
);
}
}

View File

@@ -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.
@@ -14,8 +15,14 @@ class VisitorDashboard extends StatelessWidget {
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<AuthBloc>().add(const AuthStatusChecked());
},
onPressed: () => context.read<AuthBloc>().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<AuthBloc>().add(const AuthLogoutRequested());
},
icon: const Icon(Icons.logout_rounded, size: 18),
label: const Text(
onPressed: () => context.read<AuthBloc>().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',
Text('UnionFlow',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
Text(
'Compte en attente',
color: Theme.of(context).colorScheme.onSurface,
)),
Text('Compte en attente',
style: TextStyle(
fontSize: 11,
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(
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)),
),
],
);