feat(mobile): Contribution Totale + KPI dashboard membre

- 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
This commit is contained in:
dahoud
2026-03-09 19:58:39 +00:00
parent 0a9dece955
commit 553e731a51
4 changed files with 613 additions and 260 deletions

View File

@@ -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<AuthBloc, AuthState>(
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<DashboardBloc, DashboardState>(
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<void>(
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<void>(
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<void>(
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<void>(
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<void>(
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<void>(
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';
}
}