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

@@ -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<String, dynamic> 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;
}
}

View File

@@ -1,12 +1,15 @@
import 'package:injectable/injectable.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import '../../domain/entities/dashboard_entity.dart'; import '../../domain/entities/dashboard_entity.dart';
import '../../domain/repositories/dashboard_repository.dart'; import '../../domain/repositories/dashboard_repository.dart';
import '../datasources/dashboard_remote_datasource.dart'; import '../datasources/dashboard_remote_datasource.dart';
import '../models/dashboard_stats_model.dart'; import '../models/dashboard_stats_model.dart';
import '../models/membre_dashboard_synthese_model.dart';
import '../../../../core/error/exceptions.dart'; import '../../../../core/error/exceptions.dart';
import '../../../../core/error/failures.dart'; import '../../../../core/error/failures.dart';
import '../../../../core/network/network_info.dart'; import '../../../../core/network/network_info.dart';
@LazySingleton(as: DashboardRepository)
class DashboardRepositoryImpl implements DashboardRepository { class DashboardRepositoryImpl implements DashboardRepository {
final DashboardRemoteDataSource remoteDataSource; final DashboardRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo; final NetworkInfo networkInfo;
@@ -21,8 +24,16 @@ class DashboardRepositoryImpl implements DashboardRepository {
String organizationId, String organizationId,
String userId, String userId,
) async { ) async {
if (await networkInfo.isConnected) { if (!await networkInfo.isConnected) {
return const Left(NetworkFailure('No internet connection'));
}
try { 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); final dashboardData = await remoteDataSource.getDashboardData(organizationId, userId);
return Right(_mapToEntity(dashboardData)); return Right(_mapToEntity(dashboardData));
} on ServerException catch (e) { } on ServerException catch (e) {
@@ -30,9 +41,38 @@ class DashboardRepositoryImpl implements DashboardRepository {
} catch (e) { } catch (e) {
return Left(ServerFailure('Unexpected error: $e')); return Left(ServerFailure('Unexpected error: $e'));
} }
} else {
return const Left(NetworkFailure('No internet connection'));
} }
/// 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: <String, dynamic>{},
organizationId: '',
userId: userId,
);
} }
@override @override
@@ -122,11 +162,14 @@ class DashboardRepositoryImpl implements DashboardRepository {
upcomingEvents: model.upcomingEvents, upcomingEvents: model.upcomingEvents,
totalContributions: model.totalContributions, totalContributions: model.totalContributions,
totalContributionAmount: model.totalContributionAmount, totalContributionAmount: model.totalContributionAmount,
contributionsAmountOnly: null,
pendingRequests: model.pendingRequests, pendingRequests: model.pendingRequests,
completedProjects: model.completedProjects, completedProjects: model.completedProjects,
monthlyGrowth: model.monthlyGrowth, monthlyGrowth: model.monthlyGrowth,
engagementRate: model.engagementRate, engagementRate: model.engagementRate,
lastUpdated: model.lastUpdated, lastUpdated: model.lastUpdated,
totalOrganizations: null,
organizationTypeDistribution: null,
); );
} }

View File

@@ -8,11 +8,15 @@ class DashboardStatsEntity extends Equatable {
final int upcomingEvents; final int upcomingEvents;
final int totalContributions; final int totalContributions;
final double totalContributionAmount; final double totalContributionAmount;
/// Montant des cotisations seules (sans épargne), pour la carte « Contribution Totale » membre.
final double? contributionsAmountOnly;
final int pendingRequests; final int pendingRequests;
final int completedProjects; final int completedProjects;
final double monthlyGrowth; final double monthlyGrowth;
final double engagementRate; final double engagementRate;
final DateTime lastUpdated; final DateTime lastUpdated;
final int? totalOrganizations;
final Map<String, int>? organizationTypeDistribution;
const DashboardStatsEntity({ const DashboardStatsEntity({
required this.totalMembers, required this.totalMembers,
@@ -21,11 +25,14 @@ class DashboardStatsEntity extends Equatable {
required this.upcomingEvents, required this.upcomingEvents,
required this.totalContributions, required this.totalContributions,
required this.totalContributionAmount, required this.totalContributionAmount,
this.contributionsAmountOnly,
required this.pendingRequests, required this.pendingRequests,
required this.completedProjects, required this.completedProjects,
required this.monthlyGrowth, required this.monthlyGrowth,
required this.engagementRate, required this.engagementRate,
required this.lastUpdated, required this.lastUpdated,
this.totalOrganizations,
this.organizationTypeDistribution,
}); });
// Méthodes utilitaires // Méthodes utilitaires
@@ -50,11 +57,14 @@ class DashboardStatsEntity extends Equatable {
upcomingEvents, upcomingEvents,
totalContributions, totalContributions,
totalContributionAmount, totalContributionAmount,
contributionsAmountOnly,
pendingRequests, pendingRequests,
completedProjects, completedProjects,
monthlyGrowth, monthlyGrowth,
engagementRate, engagementRate,
lastUpdated, lastUpdated,
totalOrganizations,
organizationTypeDistribution,
]; ];
} }
@@ -148,6 +158,12 @@ class UpcomingEventEntity extends Equatable {
bool get isFull => currentParticipants >= maxParticipants; bool get isFull => currentParticipants >= maxParticipants;
double get fillPercentage => maxParticipants > 0 ? currentParticipants / maxParticipants : 0.0; 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 { String get daysUntilEvent {
final now = DateTime.now(); final now = DateTime.now();
final difference = startDate.difference(now); final difference = startDate.difference(now);
@@ -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 { bool get isToday {
final now = DateTime.now(); final now = DateTime.now();
return startDate.year == now.year && return startDate.year == now.year &&

View File

@@ -1,179 +1,340 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.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 { class ActiveMemberDashboard extends StatelessWidget {
const ActiveMemberDashboard({super.key}); const ActiveMemberDashboard({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
appBar: _buildAppBar(),
body: AfricanPatternBackground(
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
final user = (authState is AuthAuthenticated) ? authState.user : null;
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( return SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// En-tête de bienvenue // En-tête
Container( AnimatedFadeIn(
width: double.infinity, delay: const Duration(milliseconds: 100),
padding: const EdgeInsets.all(20), child: _buildUserHeader(user),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00CEC9)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(16), 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,
), ),
child: const Column( ),
crossAxisAlignment: CrossAxisAlignment.start, const SizedBox(height: 24),
// Stats en grille (données backend réelles)
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: Row(
children: [ children: [
Text( Expanded(
'Bonjour !', child: UnionStatWidget(
style: TextStyle( label: 'Cotisations',
color: Colors.white, value: '${stats?.totalContributions ?? 0}',
fontSize: 24, icon: Icons.check_circle,
fontWeight: FontWeight.bold, color: UnionFlowColors.success,
trend: stats != null && stats.monthlyGrowth > 0
? '+${stats.monthlyGrowth.toStringAsFixed(0)}%'
: null,
isTrendUp: (stats?.monthlyGrowth ?? 0) > 0,
), ),
), ),
SizedBox(height: 8), const SizedBox(width: 12),
Text( Expanded(
'Bienvenue sur votre espace membre', child: UnionStatWidget(
style: TextStyle( label: 'Engagement',
color: Colors.white70, value: stats != null
fontSize: 16, ? '${(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), const SizedBox(height: 24),
// Statistiques rapides // Activité récente (données backend)
const Text( if (dashboardData != null && dashboardData.hasRecentActivity) ...[
'Mes Statistiques', AnimatedFadeIn(
delay: const Duration(milliseconds: 500),
child: const Text(
'Activité Récente',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
AnimatedSlideIn(
GridView.count( delay: const Duration(milliseconds: 600),
shrinkWrap: true, child: Column(
physics: const NeverScrollableScrollPhysics(), children: dashboardData.recentActivities.take(3).map((activity) =>
crossAxisCount: 2, Container(
childAspectRatio: 1.2, margin: const EdgeInsets.only(bottom: 12),
crossAxisSpacing: 16, padding: const EdgeInsets.all(12),
mainAxisSpacing: 16, decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: UnionFlowColors.border, width: 1),
),
child: Row(
children: [ children: [
_buildStatCard( CircleAvatar(
icon: Icons.event_available, radius: 20,
value: '12', backgroundColor: activity.type == 'contribution'
title: 'Événements', ? UnionFlowColors.success.withOpacity(0.2)
color: const Color(0xFF00B894), : 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,
), ),
_buildStatCard(
icon: Icons.volunteer_activism,
value: '3',
title: 'Solidarité',
color: const Color(0xFF00CEC9),
), ),
_buildStatCard( const SizedBox(width: 12),
icon: Icons.payment, Expanded(
value: 'À jour', child: Column(
title: 'Cotisations', crossAxisAlignment: CrossAxisAlignment.start,
color: const Color(0xFF0984E3), children: [
Text(
activity.title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
), ),
_buildStatCard( maxLines: 1,
icon: Icons.star, overflow: TextOverflow.ellipsis,
value: '4.8', ),
title: 'Engagement', const SizedBox(height: 2),
color: const Color(0xFFE17055), 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), const SizedBox(height: 24),
],
// Actions rapides // Actions rapides
const Text( AnimatedFadeIn(
delay: const Duration(milliseconds: 700),
child: const Text(
'Actions Rapides', 'Actions Rapides',
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
GridView.count( AnimatedSlideIn(
shrinkWrap: true, delay: const Duration(milliseconds: 800),
physics: const NeverScrollableScrollPhysics(), child: Row(
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [ children: [
_buildActionCard( Expanded(
icon: Icons.event, child: UnionActionButton(
title: 'Créer Événement', label: 'Cotiser',
color: const Color(0xFF00B894), icon: Icons.payment,
onTap: () {}, onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const CotisationsPageWrapper(),
), ),
_buildActionCard( );
icon: Icons.volunteer_activism, },
title: 'Demande Aide', backgroundColor: UnionFlowColors.unionGreen,
color: const Color(0xFF00CEC9),
onTap: () {},
), ),
_buildActionCard(
icon: Icons.account_circle,
title: 'Mon Profil',
color: const Color(0xFF0984E3),
onTap: () {},
), ),
_buildActionCard( const SizedBox(width: 12),
icon: Icons.message, Expanded(
title: 'Contacter', child: UnionActionButton(
color: const Color(0xFFE17055), label: 'Épargner',
onTap: () {}, 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: 24),
// Activités récentes
const Text(
'Activités Récentes',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
), ),
), const SizedBox(height: 12),
const SizedBox(height: 16),
Card( AnimatedSlideIn(
child: Column( delay: const Duration(milliseconds: 900),
child: Row(
children: [ children: [
_buildActivityItem( Expanded(
icon: Icons.check_circle, child: UnionActionButton(
title: 'Participation confirmée', label: 'Événements',
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, icon: Icons.event,
title: 'Événement créé', onTap: () {
subtitle: 'Sortie ski de fond - Il y a 3j', Navigator.of(context).push(
color: const Color(0xFF00CEC9), 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,
),
), ),
], ],
), ),
@@ -181,95 +342,141 @@ class ActiveMemberDashboard extends StatelessWidget {
], ],
), ),
); );
},
);
},
),
),
);
} }
Widget _buildStatCard({ PreferredSizeWidget _buildAppBar() {
required IconData icon, return AppBar(
required String value, backgroundColor: UnionFlowColors.surface,
required String title, elevation: 0,
required Color color, title: Row(
}) { children: [
return Card( Container(
elevation: 2, width: 32,
child: Padding( height: 32,
padding: const EdgeInsets.all(16), decoration: BoxDecoration(
child: Column( gradient: UnionFlowColors.primaryGradient,
mainAxisAlignment: MainAxisAlignment.center, 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: [ children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text( Text(
value, 'UnionFlow',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
Text(
'Membre Actif',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary,
),
),
],
),
],
),
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( style: const TextStyle(
fontSize: 24, fontSize: 18,
fontWeight: FontWeight.bold, 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), const SizedBox(height: 4),
Text( Text(
title, 'Depuis ${user?.createdAt.year ?? 2024} • Très Actif',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey[600], color: Colors.white.withOpacity(0.9),
), ),
textAlign: TextAlign.center,
), ),
], ],
), ),
), ),
); Container(
} padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
Widget _buildActionCard({ color: Colors.white,
required IconData icon,
required String title,
required Color color,
required VoidCallback onTap,
}) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
), ),
textAlign: TextAlign.center, child: const Text(
'ACTIF',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w800,
color: UnionFlowColors.gold,
letterSpacing: 0.5,
),
),
), ),
], ],
), ),
),
),
); );
} }
Widget _buildActivityItem({ String _formatAmount(double amount) {
required IconData icon, if (amount >= 1000000) {
required String title, return '${(amount / 1000000).toStringAsFixed(1)}M FCFA';
required String subtitle, } else if (amount >= 1000) {
required Color color, return '${(amount / 1000).toStringAsFixed(0)}K FCFA';
}) { }
return ListTile( return '${amount.toStringAsFixed(0)} FCFA';
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),
);
} }
} }