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 createState() => _ConnectedDashboardPageState(); } class _ConnectedDashboardPageState extends State 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().add(LoadDashboardData( organizationId: widget.organizationId, userId: widget.userId, )); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: UnionFlowColors.background, appBar: _buildAppBar(), body: AfricanPatternBackground( child: BlocBuilder( builder: (context, state) { if (state is DashboardLoading) { return const Center( child: CircularProgressIndicator(color: UnionFlowColors.unionGreen), ); } 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: UnionFlowColors.surface, 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: UnionFlowColors.textPrimary, ), ), Text( 'Dashboard', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w400, color: UnionFlowColors.textSecondary, ), ), ], ), ], ), 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: UnionFlowColors.textPrimary, 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: UnionFlowColors.unionGreen, unselectedLabelColor: UnionFlowColors.textSecondary, indicatorColor: UnionFlowColors.unionGreen, 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().add(LoadDashboardData( organizationId: widget.organizationId, userId: widget.userId, )); }, color: UnionFlowColors.unionGreen, 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(24), 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: UnionFlowColors.unionGreen, 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: 24), // 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: 24), // 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(builder: (_) => const ContributionsPageWrapper()), ); }, backgroundColor: UnionFlowColors.unionGreenPale, iconColor: UnionFlowColors.unionGreen, ), UnionActionButton( icon: Icons.send, label: 'Envoyer', onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ContributionsPageWrapper()), ); }, backgroundColor: UnionFlowColors.goldPale, iconColor: UnionFlowColors.gold, ), UnionActionButton( icon: Icons.download, label: 'Retirer', onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const EpargnePage()), ); }, backgroundColor: UnionFlowColors.terracottaPale, iconColor: UnionFlowColors.terracotta, ), UnionActionButton( icon: Icons.add_circle_outline, label: 'Créer', onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const EventsPageWrapper()), ); }, backgroundColor: UnionFlowColors.infoPale, iconColor: UnionFlowColors.info, ), ], ), ), const SizedBox(height: 24), // Activité récente - Animée AnimatedFadeIn( delay: const Duration(milliseconds: 600), child: UnionTransactionCard( title: 'Activité Récente', onSeeAll: () { Navigator.of(context).push( MaterialPageRoute(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(24), 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: UnionFlowColors.unionGreen, ); }, ), ), 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: UnionFlowColors.unionGreen, 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: 24), // Titre AnimatedFadeIn( delay: const Duration(milliseconds: 400), child: const Text( 'Métriques Financières', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary, ), ), ), const SizedBox(height: 16), // 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(24), 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(builder: (_) => const ContributionsPageWrapper()), ); }, transactions: tiles, ), ), ], ), ); } Future _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 _buildEvolutionSpots(double totalAmount, double monthlyGrowth) { final spots = []; 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(16), decoration: BoxDecoration( color: UnionFlowColors.surface, borderRadius: BorderRadius.circular(16), boxShadow: UnionFlowColors.softShadow, ), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, size: 24, color: color), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: UnionFlowColors.textSecondary, ), ), const SizedBox(height: 4), Text( value, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: color, ), ), ], ), ), ], ), ); } Widget _buildMemberNotRegisteredState() { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( gradient: UnionFlowColors.primaryGradient, shape: BoxShape.circle, ), child: const Icon( Icons.person_add_alt_1_outlined, size: 56, color: Colors.white, ), ), const SizedBox(height: 28), const Text( 'Bienvenue dans UnionFlow', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: UnionFlowColors.textPrimary, ), textAlign: TextAlign.center, ), const SizedBox(height: 12), 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: 14, color: UnionFlowColors.textSecondary, height: 1.5, ), textAlign: TextAlign.center, ), const SizedBox(height: 32), Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( color: UnionFlowColors.unionGreen.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: UnionFlowColors.unionGreen.withOpacity(0.3)), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_outline, size: 18, color: UnionFlowColors.unionGreen), SizedBox(width: 10), Flexible( child: Text( 'Contactez votre administrateur si ce message persiste.', style: TextStyle( fontSize: 13, color: UnionFlowColors.unionGreen, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ), ), ); } Widget _buildErrorState(String message) { return Center( child: AnimatedFadeIn( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: UnionFlowColors.errorPale, shape: BoxShape.circle, ), child: const Icon( Icons.error_outline, size: 64, color: UnionFlowColors.error, ), ), const SizedBox(height: 24), const Text( 'Erreur de chargement', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: UnionFlowColors.textPrimary, ), ), const SizedBox(height: 8), Text( message, style: const TextStyle( fontSize: 13, color: UnionFlowColors.textSecondary, ), textAlign: TextAlign.center, ), const SizedBox(height: 24), UFPrimaryButton( onPressed: () { context.read().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'; } }