Files
unionflow-mobile-apps/lib/features/dashboard/presentation/pages/connected_dashboard_page.dart
2026-03-31 09:14:47 +00:00

761 lines
24 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../shared/design_system/unionflow_design_v2.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../epargne/presentation/pages/epargne_page.dart';
import '../../../events/presentation/pages/events_page_wrapper.dart';
import '../bloc/dashboard_bloc.dart';
import '../../domain/entities/dashboard_entity.dart';
/// Page dashboard connectée au backend - Design UnionFlow Animé
class ConnectedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
const ConnectedDashboardPage({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<ConnectedDashboardPage> createState() => _ConnectedDashboardPageState();
}
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
PeriodFilter _selectedPeriod = PeriodFilter.month;
int _unreadNotifications = 5;
bool _isExporting = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
appBar: _buildAppBar(),
body: AfricanPatternBackground(
child: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.primaryGreen),
);
}
if (state is DashboardMemberNotRegistered) {
return _buildMemberNotRegisteredState();
}
if (state is DashboardError) {
return _buildErrorState(state.message);
}
if (state is DashboardLoaded) {
return _buildDashboardContent(state);
}
return const SizedBox.shrink();
},
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: AppColors.lightSurface,
elevation: 0,
title: Row(
children: [
Hero(
tag: 'unionflow_logo',
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: const Text(
'U',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UnionFlow',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
Text(
'Dashboard',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: AppColors.textSecondaryLight,
),
),
],
),
],
),
automaticallyImplyLeading: false,
actions: [
UnionExportButton(
isLoading: _isExporting,
onExport: (exportType) {
showDialog(
context: context,
builder: (context) => ExportConfirmDialog(
exportType: exportType,
onConfirm: () => _handleExport(exportType),
),
);
},
),
const SizedBox(width: 8),
UnionNotificationBadge(
count: _unreadNotifications,
child: IconButton(
icon: const Icon(Icons.notifications_outlined),
color: AppColors.textPrimaryLight,
onPressed: () {
setState(() => _unreadNotifications = 0);
UnionNotificationToast.show(
context,
title: 'Notifications',
message: 'Aucune nouvelle notification',
icon: Icons.notifications_active,
color: UnionFlowColors.info,
);
},
),
),
const SizedBox(width: 8),
],
bottom: TabBar(
controller: _tabController,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicatorColor: AppColors.primaryGreen,
labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
tabs: const [
Tab(text: 'Vue d\'ensemble'),
Tab(text: 'Analytique'),
Tab(text: 'Activités'),
],
),
);
}
Widget _buildDashboardContent(DashboardLoaded state) {
final data = state.dashboardData;
return RefreshIndicator(
onRefresh: () async {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
color: AppColors.primaryGreen,
child: TabBarView(
controller: _tabController,
children: [
_buildOverviewTab(data),
_buildAnalyticsTab(data),
_buildActivitiesTab(data),
],
),
);
}
UnionTransactionTile _activityToTile(RecentActivityEntity a) {
final amount = a.metadata != null && a.metadata!['amount'] != null
? '${a.metadata!['amount']} FCFA'
: (a.title.isNotEmpty ? a.title : '-');
return UnionTransactionTile(
name: a.userName,
amount: amount,
status: a.type.isNotEmpty ? a.type : 'Confirmé',
date: a.timeAgo,
);
}
Widget _buildOverviewTab(DashboardEntity data) {
final stats = data.stats;
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Balance principale - Animée
AnimatedSlideIn(
delay: const Duration(milliseconds: 100),
child: UnionBalanceCard(
label: 'Caisse Totale',
amount: _formatAmount(stats.totalContributionAmount),
trend: stats.monthlyGrowth > 0 ? '+${(stats.monthlyGrowth * 100).toStringAsFixed(0)}% ce mois' : 'Stable',
isTrendPositive: true,
),
),
const SizedBox(height: 24),
// Stats en grille - Animées avec délai
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Membres',
value: stats.totalMembers.toString(),
icon: Icons.people_outline,
color: AppColors.primaryGreen,
trend: '+8%',
isTrendUp: true,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Actifs',
value: stats.activeMembers.toString(),
icon: Icons.check_circle_outline,
color: UnionFlowColors.success,
trend: '+5%',
isTrendUp: true,
),
),
],
),
),
const SizedBox(height: 12),
AnimatedSlideIn(
delay: const Duration(milliseconds: 300),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: stats.totalEvents.toString(),
icon: Icons.event_outlined,
color: UnionFlowColors.gold,
trend: '+3',
isTrendUp: true,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'À venir',
value: stats.upcomingEvents.toString(),
icon: Icons.calendar_today,
color: UnionFlowColors.amber,
),
),
],
),
),
const SizedBox(height: 12),
// Progression - Animée
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: UnionProgressCard(
title: 'Progression des Cotisations',
progress: 0.7,
subtitle: '70% des membres ont cotisé ce mois',
),
),
const SizedBox(height: 12),
// Actions rapides - Animées
AnimatedSlideIn(
delay: const Duration(milliseconds: 500),
begin: const Offset(0, 0.2),
child: UnionActionGrid(
actions: [
UnionActionButton(
icon: Icons.payment,
label: 'Cotiser',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
backgroundColor: UnionFlowColors.unionGreenPale,
iconColor: AppColors.primaryGreen,
),
UnionActionButton(
icon: Icons.send,
label: 'Envoyer',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
backgroundColor: UnionFlowColors.goldPale,
iconColor: UnionFlowColors.gold,
),
UnionActionButton(
icon: Icons.download,
label: 'Retirer',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const EpargnePage()),
);
},
backgroundColor: UnionFlowColors.terracottaPale,
iconColor: UnionFlowColors.terracotta,
),
UnionActionButton(
icon: Icons.add_circle_outline,
label: 'Créer',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()),
);
},
backgroundColor: UnionFlowColors.infoPale,
iconColor: UnionFlowColors.info,
),
],
),
),
const SizedBox(height: 12),
// Activité récente - Animée
AnimatedFadeIn(
delay: const Duration(milliseconds: 600),
child: UnionTransactionCard(
title: 'Activité Récente',
onSeeAll: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
transactions: data.recentActivities.take(6).map((a) => _activityToTile(a)).toList(),
),
),
],
),
);
}
Widget _buildAnalyticsTab(DashboardEntity data) {
final stats = data.stats;
final entrees = stats.totalContributionAmount;
final sorties = stats.pendingRequests * 1000.0;
final benefice = entrees - sorties;
final taux = (stats.engagementRate * 100).toStringAsFixed(0);
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filtre de période - Animé
AnimatedFadeIn(
delay: const Duration(milliseconds: 50),
child: UnionPeriodFilter(
selectedPeriod: _selectedPeriod,
onPeriodChanged: (period) {
setState(() => _selectedPeriod = period);
UnionNotificationToast.show(
context,
title: 'Période mise à jour',
message: 'Affichage pour ${period.label.toLowerCase()}',
icon: Icons.calendar_today,
color: AppColors.primaryGreen,
);
},
),
),
const SizedBox(height: 24),
// Line Chart - Animé (évolution basée sur total cotisations + croissance)
AnimatedSlideIn(
delay: const Duration(milliseconds: 100),
child: UnionLineChart(
title: 'Évolution de la Caisse',
subtitle: 'Derniers 12 mois',
spots: _buildEvolutionSpots(stats.totalContributionAmount, stats.monthlyGrowth),
),
),
const SizedBox(height: 24),
// Pie Chart - Animé
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: UnionPieChart(
title: 'Répartition des Cotisations',
subtitle: 'Par catégorie',
sections: [
UnionPieChartSection.create(
value: 40,
color: AppColors.primaryGreen,
title: '40%\nCotisations',
),
UnionPieChartSection.create(
value: 30,
color: UnionFlowColors.gold,
title: '30%\nÉpargne',
),
UnionPieChartSection.create(
value: 20,
color: UnionFlowColors.terracotta,
title: '20%\nSolidarité',
),
UnionPieChartSection.create(
value: 10,
color: UnionFlowColors.amber,
title: '10%\nAutres',
),
],
),
),
const SizedBox(height: 12),
// Titre
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: const Text(
'Métriques Financières',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
),
const SizedBox(height: 8),
// Métriques - Animées (données backend)
AnimatedSlideIn(
delay: const Duration(milliseconds: 500),
begin: const Offset(0, 0.2),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildFinanceMetric(
'Entrées',
_formatFcfa(entrees),
Icons.arrow_downward,
UnionFlowColors.success,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildFinanceMetric(
'Sorties',
_formatFcfa(sorties),
Icons.arrow_upward,
UnionFlowColors.error,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildFinanceMetric(
'Bénéfice',
_formatFcfa(benefice),
Icons.trending_up,
UnionFlowColors.gold,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildFinanceMetric(
'Taux',
'$taux%',
Icons.percent,
UnionFlowColors.info,
),
),
],
),
],
),
),
],
),
);
}
Widget _buildActivitiesTab(DashboardEntity data) {
final tiles = data.recentActivities.map((a) => _activityToTile(a)).toList();
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedSlideIn(
delay: const Duration(milliseconds: 100),
child: UnionTransactionCard(
title: 'Toutes les Activités',
onSeeAll: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
transactions: tiles,
),
),
],
),
);
}
Future<void> _handleExport(ExportType exportType) async {
setState(() => _isExporting = true);
// Simulation de l'export (dans un vrai cas, appel API ici)
await Future.delayed(const Duration(seconds: 2));
setState(() => _isExporting = false);
if (mounted) {
UnionNotificationToast.show(
context,
title: 'Export réussi',
message: 'Le rapport ${exportType.label} a été généré avec succès',
icon: Icons.check_circle,
color: UnionFlowColors.success,
);
}
}
String _formatFcfa(double value) {
if (value >= 1000000) return '${(value / 1000000).toStringAsFixed(1)}M FCFA';
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}K FCFA';
return '${value.toStringAsFixed(0)} FCFA';
}
List<FlSpot> _buildEvolutionSpots(double totalAmount, double monthlyGrowth) {
final spots = <FlSpot>[];
var v = totalAmount * 0.5;
for (var i = 0; i < 12; i++) {
spots.add(FlSpot(i.toDouble(), v));
v = v * (1 + (monthlyGrowth > 0 ? monthlyGrowth : 0.02));
}
if (spots.isNotEmpty) spots[spots.length - 1] = FlSpot(11, totalAmount);
return spots;
}
Widget _buildFinanceMetric(String label, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 16, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textSecondaryLight,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: color,
),
),
],
),
),
],
),
);
}
Widget _buildMemberNotRegisteredState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
shape: BoxShape.circle,
),
child: const Icon(
Icons.person_add_alt_1_outlined,
size: 36,
color: Colors.white,
),
),
const SizedBox(height: 14),
const Text(
'Bienvenue dans UnionFlow',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: AppColors.textPrimaryLight,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Votre compte est en cours de configuration par un administrateur. '
'Votre tableau de bord sera disponible dès que votre profil membre aura été activé.',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondaryLight,
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.primaryGreen.withOpacity(0.3)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info_outline, size: 18, color: AppColors.primaryGreen),
SizedBox(width: 10),
Flexible(
child: Text(
'Contactez votre administrateur si ce message persiste.',
style: TextStyle(
fontSize: 13,
color: AppColors.primaryGreen,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
),
);
}
Widget _buildErrorState(String message) {
return Center(
child: AnimatedFadeIn(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
shape: BoxShape.circle,
),
child: const Icon(
Icons.error_outline,
size: 40,
color: UnionFlowColors.error,
),
),
const SizedBox(height: 12),
const Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
const SizedBox(height: 8),
Text(
message,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondaryLight,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
UFPrimaryButton(
onPressed: () {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
label: 'RÉESSAYER',
),
],
),
),
);
}
String _formatAmount(num amount) {
return '${amount.toStringAsFixed(0).replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
)} FCFA';
}
}