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:
@@ -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()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user