From 553e731a51c897b450e9cf0f201b4893fe546908 Mon Sep 17 00:00:00 2001 From: dahoud Date: Mon, 9 Mar 2026 19:58:39 +0000 Subject: [PATCH] feat(mobile): Contribution Totale + KPI dashboard membre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MembreDashboardSyntheseModel: totalCotisationsPayeesToutTemps - DashboardStatsEntity: contributionsAmountOnly (cotisations seules) - Mapping: Mon Solde Total = cotisations tout temps + épargne, Contribution Totale = cotisations seules - Engagement: fallback tauxCotisationsPerso si tauxParticipation absent - Carte Contribution Totale utilise contributionsAmountOnly Made-with: Cursor --- .../membre_dashboard_synthese_model.dart | 79 ++ .../dashboard_repository_impl.dart | 63 +- .../domain/entities/dashboard_entity.dart | 26 +- .../active_member_dashboard.dart | 705 +++++++++++------- 4 files changed, 613 insertions(+), 260 deletions(-) create mode 100644 unionflow/unionflow-mobile-apps/lib/features/dashboard/data/models/membre_dashboard_synthese_model.dart diff --git a/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/models/membre_dashboard_synthese_model.dart b/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/models/membre_dashboard_synthese_model.dart new file mode 100644 index 0000000..833aaca --- /dev/null +++ b/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/models/membre_dashboard_synthese_model.dart @@ -0,0 +1,79 @@ +/// Modèle pour la réponse GET /api/dashboard/membre/me (backend MembreDashboardSyntheseResponse). +/// Utilisé quand l'utilisateur est un membre sans organisationId (dashboard personnel). +class MembreDashboardSyntheseModel { + final String prenom; + final String nom; + final String? dateInscription; // ISO date string + final double mesCotisationsPaiement; + /// Total des cotisations payées sur l'année (pour dashboard). + final double totalCotisationsPayeesAnnee; + /// Total des cotisations payées tout temps (pour carte « Contribution Totale »). + final double totalCotisationsPayeesToutTemps; + /// Nombre de cotisations payées (pour carte « Cotisations »). + final int nombreCotisationsPayees; + final String statutCotisations; + final int? tauxCotisationsPerso; + final double monSoldeEpargne; + final double evolutionEpargneNombre; + final String evolutionEpargne; + final int objectifEpargne; + final int mesEvenementsInscrits; + final int evenementsAVenir; + final int? tauxParticipationPerso; + final int mesDemandesAide; + final int aidesEnCours; + final int? tauxAidesApprouvees; + + const MembreDashboardSyntheseModel({ + required this.prenom, + required this.nom, + this.dateInscription, + this.mesCotisationsPaiement = 0, + this.totalCotisationsPayeesAnnee = 0, + this.totalCotisationsPayeesToutTemps = 0, + this.nombreCotisationsPayees = 0, + this.statutCotisations = 'À jour', + this.tauxCotisationsPerso, + this.monSoldeEpargne = 0, + this.evolutionEpargneNombre = 0, + this.evolutionEpargne = '+0%', + this.objectifEpargne = 0, + this.mesEvenementsInscrits = 0, + this.evenementsAVenir = 0, + this.tauxParticipationPerso, + this.mesDemandesAide = 0, + this.aidesEnCours = 0, + this.tauxAidesApprouvees, + }); + + factory MembreDashboardSyntheseModel.fromJson(Map json) { + return MembreDashboardSyntheseModel( + prenom: json['prenom'] as String? ?? '', + nom: json['nom'] as String? ?? '', + dateInscription: json['dateInscription'] as String?, + mesCotisationsPaiement: _toDouble(json['mesCotisationsPaiement']), + totalCotisationsPayeesAnnee: _toDouble(json['totalCotisationsPayeesAnnee']), + totalCotisationsPayeesToutTemps: _toDouble(json['totalCotisationsPayeesToutTemps']), + nombreCotisationsPayees: (json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0, + statutCotisations: json['statutCotisations'] as String? ?? 'À jour', + tauxCotisationsPerso: (json['tauxCotisationsPerso'] as num?)?.toInt(), + monSoldeEpargne: _toDouble(json['monSoldeEpargne']), + evolutionEpargneNombre: _toDouble(json['evolutionEpargneNombre']), + evolutionEpargne: json['evolutionEpargne'] as String? ?? '+0%', + objectifEpargne: (json['objectifEpargne'] as num?)?.toInt() ?? 0, + mesEvenementsInscrits: (json['mesEvenementsInscrits'] as num?)?.toInt() ?? 0, + evenementsAVenir: (json['evenementsAVenir'] as num?)?.toInt() ?? 0, + tauxParticipationPerso: (json['tauxParticipationPerso'] as num?)?.toInt(), + mesDemandesAide: (json['mesDemandesAide'] as num?)?.toInt() ?? 0, + aidesEnCours: (json['aidesEnCours'] as num?)?.toInt() ?? 0, + tauxAidesApprouvees: (json['tauxAidesApprouvees'] as num?)?.toInt(), + ); + } + + static double _toDouble(dynamic v) { + if (v == null) return 0; + if (v is num) return v.toDouble(); + if (v is String) return double.tryParse(v) ?? 0; + return 0; + } +} diff --git a/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/repositories/dashboard_repository_impl.dart b/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/repositories/dashboard_repository_impl.dart index a051b66..f7c8cd3 100644 --- a/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/repositories/dashboard_repository_impl.dart +++ b/unionflow/unionflow-mobile-apps/lib/features/dashboard/data/repositories/dashboard_repository_impl.dart @@ -1,12 +1,15 @@ +import 'package:injectable/injectable.dart'; import 'package:dartz/dartz.dart'; import '../../domain/entities/dashboard_entity.dart'; import '../../domain/repositories/dashboard_repository.dart'; import '../datasources/dashboard_remote_datasource.dart'; import '../models/dashboard_stats_model.dart'; +import '../models/membre_dashboard_synthese_model.dart'; import '../../../../core/error/exceptions.dart'; import '../../../../core/error/failures.dart'; import '../../../../core/network/network_info.dart'; +@LazySingleton(as: DashboardRepository) class DashboardRepositoryImpl implements DashboardRepository { final DashboardRemoteDataSource remoteDataSource; final NetworkInfo networkInfo; @@ -21,18 +24,55 @@ class DashboardRepositoryImpl implements DashboardRepository { String organizationId, String userId, ) async { - if (await networkInfo.isConnected) { - try { - final dashboardData = await remoteDataSource.getDashboardData(organizationId, userId); - return Right(_mapToEntity(dashboardData)); - } on ServerException catch (e) { - return Left(ServerFailure(e.message)); - } catch (e) { - return Left(ServerFailure('Unexpected error: $e')); - } - } else { + if (!await networkInfo.isConnected) { return const Left(NetworkFailure('No internet connection')); } + try { + // Membre sans contexte org : utiliser l'API dashboard membre (GET /api/dashboard/membre/me) + final useMemberDashboard = organizationId.trim().isEmpty; + if (useMemberDashboard) { + final synthese = await remoteDataSource.getMemberDashboardData(); + return Right(_mapMemberSyntheseToEntity(synthese, userId)); + } + final dashboardData = await remoteDataSource.getDashboardData(organizationId, userId); + return Right(_mapToEntity(dashboardData)); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(ServerFailure('Unexpected error: $e')); + } + } + + /// Construit une DashboardEntity à partir de la synthèse membre (même structure pour réutiliser l'UI). + DashboardEntity _mapMemberSyntheseToEntity(MembreDashboardSyntheseModel s, String userId) { + final now = DateTime.now(); + // Contribution Totale = cotisations payées tout temps ; MON SOLDE TOTAL = cotisations tout temps + épargne + final totalCotisationsToutTemps = s.totalCotisationsPayeesToutTemps; + final monSoldeTotal = totalCotisationsToutTemps + s.monSoldeEpargne; + final stats = DashboardStatsEntity( + totalMembers: 0, + activeMembers: 0, + totalEvents: 0, + upcomingEvents: s.evenementsAVenir, + totalContributions: s.nombreCotisationsPayees, + totalContributionAmount: monSoldeTotal, + contributionsAmountOnly: totalCotisationsToutTemps, + pendingRequests: 0, + completedProjects: 0, + monthlyGrowth: s.evolutionEpargneNombre, + engagementRate: ((s.tauxParticipationPerso ?? s.tauxCotisationsPerso) ?? 0) / 100.0, + lastUpdated: now, + totalOrganizations: null, + organizationTypeDistribution: null, + ); + return DashboardEntity( + stats: stats, + recentActivities: const [], + upcomingEvents: const [], + userPreferences: {}, + organizationId: '', + userId: userId, + ); } @override @@ -122,11 +162,14 @@ class DashboardRepositoryImpl implements DashboardRepository { upcomingEvents: model.upcomingEvents, totalContributions: model.totalContributions, totalContributionAmount: model.totalContributionAmount, + contributionsAmountOnly: null, pendingRequests: model.pendingRequests, completedProjects: model.completedProjects, monthlyGrowth: model.monthlyGrowth, engagementRate: model.engagementRate, lastUpdated: model.lastUpdated, + totalOrganizations: null, + organizationTypeDistribution: null, ); } diff --git a/unionflow/unionflow-mobile-apps/lib/features/dashboard/domain/entities/dashboard_entity.dart b/unionflow/unionflow-mobile-apps/lib/features/dashboard/domain/entities/dashboard_entity.dart index 3338cb9..8f33ada 100644 --- a/unionflow/unionflow-mobile-apps/lib/features/dashboard/domain/entities/dashboard_entity.dart +++ b/unionflow/unionflow-mobile-apps/lib/features/dashboard/domain/entities/dashboard_entity.dart @@ -8,11 +8,15 @@ class DashboardStatsEntity extends Equatable { final int upcomingEvents; final int totalContributions; final double totalContributionAmount; + /// Montant des cotisations seules (sans épargne), pour la carte « Contribution Totale » membre. + final double? contributionsAmountOnly; final int pendingRequests; final int completedProjects; final double monthlyGrowth; final double engagementRate; final DateTime lastUpdated; + final int? totalOrganizations; + final Map? organizationTypeDistribution; const DashboardStatsEntity({ required this.totalMembers, @@ -21,11 +25,14 @@ class DashboardStatsEntity extends Equatable { required this.upcomingEvents, required this.totalContributions, required this.totalContributionAmount, + this.contributionsAmountOnly, required this.pendingRequests, required this.completedProjects, required this.monthlyGrowth, required this.engagementRate, required this.lastUpdated, + this.totalOrganizations, + this.organizationTypeDistribution, }); // Méthodes utilitaires @@ -50,11 +57,14 @@ class DashboardStatsEntity extends Equatable { upcomingEvents, totalContributions, totalContributionAmount, + contributionsAmountOnly, pendingRequests, completedProjects, monthlyGrowth, engagementRate, lastUpdated, + totalOrganizations, + organizationTypeDistribution, ]; } @@ -148,10 +158,16 @@ class UpcomingEventEntity extends Equatable { bool get isFull => currentParticipants >= maxParticipants; double get fillPercentage => maxParticipants > 0 ? currentParticipants / maxParticipants : 0.0; + int get daysUntilEventInt { + final now = DateTime.now(); + final difference = startDate.difference(now); + return difference.inDays; + } + String get daysUntilEvent { final now = DateTime.now(); final difference = startDate.difference(now); - + if (difference.inDays > 0) { return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}'; } else if (difference.inHours > 0) { @@ -163,6 +179,14 @@ class UpcomingEventEntity extends Equatable { } } + String get formattedDate { + final months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', + 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc']; + return '${startDate.day} ${months[startDate.month - 1]} ${startDate.year}'; + } + + bool get hasParticipantInfo => maxParticipants > 0; + bool get isToday { final now = DateTime.now(); return startDate.year == now.year && diff --git a/unionflow/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/role_dashboards/active_member_dashboard.dart b/unionflow/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/role_dashboards/active_member_dashboard.dart index 2434505..c4a8c2a 100644 --- a/unionflow/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/role_dashboards/active_member_dashboard.dart +++ b/unionflow/unionflow-mobile-apps/lib/features/dashboard/presentation/pages/role_dashboards/active_member_dashboard.dart @@ -1,275 +1,482 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart'; +import '../../../../../shared/design_system/unionflow_design_v2.dart'; +import '../../bloc/dashboard_bloc.dart'; +import '../../../../authentication/presentation/bloc/auth_bloc.dart'; +import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart'; +import '../../../../epargne/presentation/pages/epargne_page.dart'; +import '../../../../profile/presentation/pages/profile_page_wrapper.dart'; +import '../../../../help/presentation/pages/help_support_page.dart'; +import '../../../../events/presentation/pages/events_page_wrapper.dart'; +import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart'; -/// Dashboard simple pour Membre Actif +/// Dashboard Membre Actif - Design UnionFlow Enrichi class ActiveMemberDashboard extends StatelessWidget { const ActiveMemberDashboard({super.key}); @override Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // En-tête de bienvenue - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF00B894), Color(0xFF00CEC9)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - ), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Bonjour !', - style: TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 8), - Text( - 'Bienvenue sur votre espace membre', - style: TextStyle( - color: Colors.white70, - fontSize: 16, - ), - ), - ], - ), - ), - - const SizedBox(height: 24), - - // Statistiques rapides - const Text( - 'Mes Statistiques', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 2, - childAspectRatio: 1.2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - children: [ - _buildStatCard( - icon: Icons.event_available, - value: '12', - title: 'Événements', - color: const Color(0xFF00B894), - ), - _buildStatCard( - icon: Icons.volunteer_activism, - value: '3', - title: 'Solidarité', - color: const Color(0xFF00CEC9), - ), - _buildStatCard( - icon: Icons.payment, - value: 'À jour', - title: 'Cotisations', - color: const Color(0xFF0984E3), - ), - _buildStatCard( - icon: Icons.star, - value: '4.8', - title: 'Engagement', - color: const Color(0xFFE17055), - ), - ], - ), - - const SizedBox(height: 24), - - // Actions rapides - const Text( - 'Actions Rapides', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: 2, - childAspectRatio: 1.5, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - children: [ - _buildActionCard( - icon: Icons.event, - title: 'Créer Événement', - color: const Color(0xFF00B894), - onTap: () {}, - ), - _buildActionCard( - icon: Icons.volunteer_activism, - title: 'Demande Aide', - color: const Color(0xFF00CEC9), - onTap: () {}, - ), - _buildActionCard( - icon: Icons.account_circle, - title: 'Mon Profil', - color: const Color(0xFF0984E3), - onTap: () {}, - ), - _buildActionCard( - icon: Icons.message, - title: 'Contacter', - color: const Color(0xFFE17055), - onTap: () {}, - ), - ], - ), - - const SizedBox(height: 24), - - // Activités récentes - const Text( - 'Activités Récentes', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - Card( - child: Column( - children: [ - _buildActivityItem( - icon: Icons.check_circle, - title: 'Participation confirmée', - subtitle: 'Assemblée Générale - Il y a 2h', - color: const Color(0xFF00B894), - ), - const Divider(height: 1), - _buildActivityItem( - icon: Icons.payment, - title: 'Cotisation payée', - subtitle: 'Décembre 2024 - Il y a 1j', - color: const Color(0xFF0984E3), - ), - const Divider(height: 1), - _buildActivityItem( - icon: Icons.event, - title: 'Événement créé', - subtitle: 'Sortie ski de fond - Il y a 3j', - color: const Color(0xFF00CEC9), - ), - ], - ), - ), - ], - ), - ); - } + return Scaffold( + backgroundColor: UnionFlowColors.background, + appBar: _buildAppBar(), + body: AfricanPatternBackground( + child: BlocBuilder( + builder: (context, authState) { + final user = (authState is AuthAuthenticated) ? authState.user : null; - Widget _buildStatCard({ - required IconData icon, - required String value, - required String title, - required Color color, - }) { - return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, color: color, size: 32), - const SizedBox(height: 8), - Text( - value, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - title, - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - textAlign: TextAlign.center, - ), - ], + return BlocBuilder( + builder: (context, dashboardState) { + if (dashboardState is DashboardLoading) { + return const Center( + child: CircularProgressIndicator(color: UnionFlowColors.unionGreen), + ); + } + + final dashboardData = (dashboardState is DashboardLoaded) + ? dashboardState.dashboardData + : null; + final stats = dashboardData?.stats; + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // En-tête + AnimatedFadeIn( + delay: const Duration(milliseconds: 100), + child: _buildUserHeader(user), + ), + const SizedBox(height: 24), + + // Balance principale (données backend réelles) + AnimatedSlideIn( + delay: const Duration(milliseconds: 200), + child: UnionBalanceCard( + label: 'Mon Solde Total', + amount: _formatAmount(stats?.totalContributionAmount ?? 0), + trend: stats != null && stats.monthlyGrowth != 0 + ? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois' + : 'Aucune variation', + isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0, + ), + ), + const SizedBox(height: 24), + + // Stats en grille (données backend réelles) + AnimatedFadeIn( + delay: const Duration(milliseconds: 300), + child: Row( + children: [ + Expanded( + child: UnionStatWidget( + label: 'Cotisations', + value: '${stats?.totalContributions ?? 0}', + icon: Icons.check_circle, + color: UnionFlowColors.success, + trend: stats != null && stats.monthlyGrowth > 0 + ? '+${stats.monthlyGrowth.toStringAsFixed(0)}%' + : null, + isTrendUp: (stats?.monthlyGrowth ?? 0) > 0, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UnionStatWidget( + label: 'Engagement', + value: stats != null + ? '${(stats.engagementRate * 100).toStringAsFixed(0)}%' + : '0%', + icon: Icons.trending_up, + color: UnionFlowColors.gold, + trend: stats != null && stats.engagementRate > 0.7 + ? 'Excellent' + : stats != null && stats.engagementRate > 0.5 + ? 'Bon' + : null, + isTrendUp: (stats?.engagementRate ?? 0) > 0.7, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + AnimatedFadeIn( + delay: const Duration(milliseconds: 400), + child: Row( + children: [ + Expanded( + child: UnionStatWidget( + label: 'Contribution Totale', + value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0), + icon: Icons.savings, + color: UnionFlowColors.amber, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UnionStatWidget( + label: 'Événements', + value: '${stats?.upcomingEvents ?? 0}', + icon: Icons.event_available, + color: UnionFlowColors.terracotta, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Activité récente (données backend) + if (dashboardData != null && dashboardData.hasRecentActivity) ...[ + AnimatedFadeIn( + delay: const Duration(milliseconds: 500), + child: const Text( + 'Activité Récente', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: UnionFlowColors.textPrimary, + ), + ), + ), + const SizedBox(height: 16), + AnimatedSlideIn( + delay: const Duration(milliseconds: 600), + child: Column( + children: dashboardData.recentActivities.take(3).map((activity) => + Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: UnionFlowColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: UnionFlowColors.border, width: 1), + ), + child: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: activity.type == 'contribution' + ? UnionFlowColors.success.withOpacity(0.2) + : activity.type == 'event' + ? UnionFlowColors.gold.withOpacity(0.2) + : UnionFlowColors.indigo.withOpacity(0.2), + child: Icon( + activity.type == 'contribution' + ? Icons.payment + : activity.type == 'event' + ? Icons.event + : Icons.person_add, + size: 18, + color: activity.type == 'contribution' + ? UnionFlowColors.success + : activity.type == 'event' + ? UnionFlowColors.gold + : UnionFlowColors.indigo, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + activity.title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: UnionFlowColors.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + activity.description, + style: const TextStyle( + fontSize: 11, + color: UnionFlowColors.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Text( + activity.timeAgo, + style: const TextStyle( + fontSize: 11, + color: UnionFlowColors.textTertiary, + ), + ), + ], + ), + ) + ).toList(), + ), + ), + const SizedBox(height: 24), + ], + + // Actions rapides + AnimatedFadeIn( + delay: const Duration(milliseconds: 700), + child: const Text( + 'Actions Rapides', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: UnionFlowColors.textPrimary, + ), + ), + ), + const SizedBox(height: 16), + + AnimatedSlideIn( + delay: const Duration(milliseconds: 800), + child: Row( + children: [ + Expanded( + child: UnionActionButton( + label: 'Cotiser', + icon: Icons.payment, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const CotisationsPageWrapper(), + ), + ); + }, + backgroundColor: UnionFlowColors.unionGreen, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UnionActionButton( + label: 'Épargner', + icon: Icons.savings_outlined, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const EpargnePage(), + ), + ); + }, + backgroundColor: UnionFlowColors.gold, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UnionActionButton( + label: 'Crédit', + icon: Icons.account_balance_wallet, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const EpargnePage(), + ), + ); + }, + backgroundColor: UnionFlowColors.amber, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + + AnimatedSlideIn( + delay: const Duration(milliseconds: 900), + child: Row( + children: [ + Expanded( + child: UnionActionButton( + label: 'Événements', + icon: Icons.event, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const EventsPageWrapper(), + ), + ); + }, + backgroundColor: UnionFlowColors.terracotta, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UnionActionButton( + label: 'Solidarité', + icon: Icons.favorite_outline, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const DemandesAidePageWrapper(), + ), + ); + }, + backgroundColor: UnionFlowColors.error, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UnionActionButton( + label: 'Profil', + icon: Icons.person_outline, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const ProfilePageWrapper(), + ), + ); + }, + backgroundColor: UnionFlowColors.indigo, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + }, ), ), ); } - Widget _buildActionCard({ - required IconData icon, - required String title, - required Color color, - required VoidCallback onTap, - }) { - return Card( - elevation: 2, - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: UnionFlowColors.surface, + elevation: 0, + title: Row( + children: [ + 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: [ - Icon(icon, color: color, size: 28), - const SizedBox(height: 8), Text( - title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + 'UnionFlow', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: UnionFlowColors.textPrimary, + ), + ), + Text( + 'Membre Actif', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: UnionFlowColors.textSecondary, ), - textAlign: TextAlign.center, ), ], ), + ], + ), + automaticallyImplyLeading: false, + ); + } + + Widget _buildUserHeader(dynamic user) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: UnionFlowColors.warmGradient, + borderRadius: BorderRadius.circular(16), + border: const Border( + top: BorderSide(color: UnionFlowColors.gold, width: 3), ), + boxShadow: UnionFlowColors.goldGlowShadow, + ), + child: Row( + children: [ + CircleAvatar( + radius: 28, + backgroundColor: Colors.white.withOpacity(0.3), + child: Text( + user?.initials ?? 'MA', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user?.fullName ?? 'Membre Actif', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + 'Depuis ${user?.createdAt.year ?? 2024} • Très Actif', + style: TextStyle( + fontSize: 12, + color: Colors.white.withOpacity(0.9), + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'ACTIF', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w800, + color: UnionFlowColors.gold, + letterSpacing: 0.5, + ), + ), + ), + ], ), ); } - Widget _buildActivityItem({ - required IconData icon, - required String title, - required String subtitle, - required Color color, - }) { - return ListTile( - leading: CircleAvatar( - backgroundColor: color.withOpacity(0.1), - child: Icon(icon, color: color, size: 20), - ), - title: Text( - title, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text(subtitle), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - ); + String _formatAmount(double amount) { + if (amount >= 1000000) { + return '${(amount / 1000000).toStringAsFixed(1)}M FCFA'; + } else if (amount >= 1000) { + return '${(amount / 1000).toStringAsFixed(0)}K FCFA'; + } + return '${amount.toStringAsFixed(0)} FCFA'; } }