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,252 +15,302 @@ 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>(
builder: (context, authState) { listener: (context, state) {
final user = (authState is AuthAuthenticated) ? authState.user : null; if (state is DashboardLoaded || state is DashboardError) {
return BlocBuilder<DashboardBloc, DashboardState>( _refreshCompleter?.complete();
builder: (context, dashboardState) { _refreshCompleter = null;
if (dashboardState is DashboardLoading) { }
return const Center( },
child: CircularProgressIndicator( child: BlocBuilder<AuthBloc, AuthState>(
color: UnionFlowColors.error, builder: (context, authState) {
strokeWidth: 2, final user = (authState is AuthAuthenticated) ? authState.user : null;
), return BlocBuilder<DashboardBloc, DashboardState>(
); builder: (context, dashboardState) {
} if (dashboardState is DashboardLoading ||
final dashboardData = (dashboardState is DashboardLoaded) dashboardState is DashboardInitial) {
? dashboardState.dashboardData return const Center(
: null; child: CircularProgressIndicator(
final stats = dashboardData?.stats; color: ModuleColors.systeme,
strokeWidth: 2,
),
);
}
return RefreshIndicator( final dashboardData = dashboardState is DashboardLoaded
color: UnionFlowColors.error, ? dashboardState.dashboardData
strokeWidth: 2, : dashboardState is DashboardRefreshing
onRefresh: () async { ? dashboardState.dashboardData
context.read<DashboardBloc>().add(LoadDashboardData( : null;
organizationId: dashboardData?.organizationId ?? '', final stats = dashboardData?.stats;
userId: user?.id ?? '',
useGlobalDashboard: true, return RefreshIndicator(
)); color: ModuleColors.systeme,
await Future.delayed(const Duration(milliseconds: 600)); strokeWidth: 2,
}, onRefresh: () async {
child: SingleChildScrollView( _refreshCompleter = Completer<void>();
physics: const AlwaysScrollableScrollPhysics(), context.read<DashboardBloc>().add(LoadDashboardData(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), organizationId: dashboardData?.organizationId ?? '',
child: Column( userId: user?.id ?? '',
crossAxisAlignment: CrossAxisAlignment.start, useGlobalDashboard: true,
children: [ ));
// ── Carte identité ─────────────────────────────── return _refreshCompleter!.future;
UserIdentityCard( },
initials: user?.initials ?? 'SA', child: SingleChildScrollView(
name: user?.fullName ?? 'Super Administrateur', physics: const AlwaysScrollableScrollPhysics(),
subtitle: 'Accès global système', padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
badgeLabel: 'ROOT', child: Column(
gradient: const LinearGradient( crossAxisAlignment: CrossAxisAlignment.start,
colors: [Color(0xFFB91C1C), Color(0xFF7F1D1D)], children: [
begin: Alignment.centerLeft, // ── Carte identité ROOT ──────────────────────────
end: Alignment.centerRight, 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, const SizedBox(height: 12),
showTopBorder: false,
),
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),
trend: stats != null && stats.totalContributionAmount > 0 trend: stats != null && stats.totalContributionAmount > 0
? '${stats.monthlyGrowth >= 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}%' ? '${stats.monthlyGrowth >= 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}%'
: null, : null,
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0, isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
), ),
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(),
crossAxisCount: 3, crossAxisCount: 3,
mainAxisSpacing: 6, mainAxisSpacing: 6,
crossAxisSpacing: 6, crossAxisSpacing: 6,
childAspectRatio: 2.0, childAspectRatio: 2.0,
children: [ children: [
UnionStatWidget( UnionStatWidget(
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,
), ),
UnionStatWidget( UnionStatWidget(
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,
), ),
UnionStatWidget( UnionStatWidget(
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',
value: stats != null value: stats != null
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%' ? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
: '', : '',
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),
// ── Activité membres ─────────────────────────────
if (stats != null && stats.totalMembers > 0) ...[
_MemberActivityCard(stats: stats),
const SizedBox(height: 12),
], ],
),
const SizedBox(height: 12),
// ── Activité membres (pie compact) ─────────────── // ── Répartition types d'org ──────────────────────
if (stats != null && stats.totalMembers > 0) ...[ if (stats?.organizationTypeDistribution != null &&
_buildMemberActivityRow(stats), stats!.organizationTypeDistribution!.isNotEmpty) ...[
const SizedBox(height: 12), _OrgTypeCard(stats: stats),
], const SizedBox(height: 12),
],
// ── Répartition types d'org ────────────────────── // ── Événements à venir ───────────────────────────
if (stats?.organizationTypeDistribution != null &&
stats!.organizationTypeDistribution!.isNotEmpty) ...[
_buildOrgTypeRow(stats),
const SizedBox(height: 12),
],
// ── É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),
...dashboardData.upcomingEvents.take(3).map( if (dashboardData != null && dashboardData.hasUpcomingEvents)
(e) => DashboardEventRow( ...dashboardData.upcomingEvents.take(3).map(
title: e.title, (e) => DashboardEventRow(
date: e.formattedDate, title: e.title,
daysUntil: e.daysUntilEvent, date: e.formattedDate,
participants: daysUntil: e.daysUntilEvent,
'${e.currentParticipants}/${e.maxParticipants}', participants:
), '${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),
...dashboardData.recentActivities.take(5).map( if (dashboardData != null && dashboardData.hasRecentActivity)
(a) => DashboardActivityRow( ...dashboardData.recentActivities.take(5).map(
title: a.title, (a) => DashboardActivityRow(
description: a.description, title: a.title,
timeAgo: a.timeAgo, description: a.description,
icon: DashboardActivityRow.iconFor(a.type), timeAgo: a.timeAgo,
color: DashboardActivityRow.colorFor(a.type), 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), 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,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3, crossAxisCount: 3,
mainAxisSpacing: 6, mainAxisSpacing: 6,
crossAxisSpacing: 6, crossAxisSpacing: 6,
childAspectRatio: 2.8, childAspectRatio: 2.8,
children: [ children: [
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(
UnionActionButton( builder: (_) =>
label: 'Utilisateurs', const OrganizationsPageWrapper())),
icon: Icons.people_rounded, ),
iconColor: UnionFlowColors.gold, UnionActionButton(
onTap: () => Navigator.push(context, label: 'Utilisateurs',
MaterialPageRoute(builder: (_) => const UserManagementPage())), icon: Icons.people_rounded,
), iconColor: ModuleColors.membres,
UnionActionButton( onTap: () => Navigator.push(
label: 'Système', context,
icon: Icons.settings_rounded, MaterialPageRoute(
iconColor: UnionFlowColors.indigo, builder: (_) =>
onTap: () => Navigator.push(context, const UserManagementPage())),
MaterialPageRoute(builder: (_) => const SystemSettingsPage())), ),
), UnionActionButton(
UnionActionButton( label: 'Système',
label: 'Backup', icon: Icons.settings_rounded,
icon: Icons.backup_rounded, iconColor: ModuleColors.systeme,
iconColor: UnionFlowColors.warning, onTap: () => Navigator.push(
onTap: () => Navigator.push(context, context,
MaterialPageRoute(builder: (_) => const BackupPage())), MaterialPageRoute(
), builder: (_) =>
UnionActionButton( const SystemSettingsPage())),
label: 'Sécurité', ),
icon: Icons.security_rounded, UnionActionButton(
iconColor: UnionFlowColors.error, label: 'Backup',
onTap: () => Navigator.push(context, icon: Icons.backup_rounded,
MaterialPageRoute(builder: (_) => const SystemSettingsPage())), iconColor: ModuleColors.backup,
), onTap: () => Navigator.push(
UnionActionButton( context,
label: 'Support', MaterialPageRoute(
icon: Icons.help_outline_rounded, builder: (_) => const BackupPage())),
iconColor: UnionFlowColors.info, ),
onTap: () => Navigator.push(context, // Bouton "Analytics" retiré : redondant avec
MaterialPageRoute(builder: (_) => const HelpSupportPage())), // 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 ──────────────────────────────────────────────────────────────── // ─── 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.
@@ -11,11 +12,17 @@ class VisitorDashboard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final authState = context.watch<AuthBloc>().state; final authState = context.watch<AuthBloc>().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 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: Theme.of(context).colorScheme.onSurface,
color: UnionFlowColors.textPrimary, )),
), Text('Compte en attente',
), style: TextStyle(
Text( fontSize: 11,
'Compte en attente', fontWeight: FontWeight.w400,
style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 11, )),
fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary,
),
),
], ],
), ),
], ],
), ),
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,
),
),
), ),
], ],
); );