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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,18 +24,55 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
|||||||
String organizationId,
|
String organizationId,
|
||||||
String userId,
|
String userId,
|
||||||
) async {
|
) async {
|
||||||
if (await networkInfo.isConnected) {
|
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 {
|
|
||||||
return const Left(NetworkFailure('No internet connection'));
|
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: <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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +158,16 @@ 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);
|
||||||
|
|
||||||
if (difference.inDays > 0) {
|
if (difference.inDays > 0) {
|
||||||
return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
|
return '${difference.inDays} jour${difference.inDays > 1 ? 's' : ''}';
|
||||||
} else if (difference.inHours > 0) {
|
} 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 {
|
bool get isToday {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
return startDate.year == now.year &&
|
return startDate.year == now.year &&
|
||||||
|
|||||||
@@ -1,275 +1,482 @@
|
|||||||
|
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 SingleChildScrollView(
|
return Scaffold(
|
||||||
padding: const EdgeInsets.all(16),
|
backgroundColor: UnionFlowColors.background,
|
||||||
child: Column(
|
appBar: _buildAppBar(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
body: AfricanPatternBackground(
|
||||||
children: [
|
child: BlocBuilder<AuthBloc, AuthState>(
|
||||||
// En-tête de bienvenue
|
builder: (context, authState) {
|
||||||
Container(
|
final user = (authState is AuthAuthenticated) ? authState.user : null;
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatCard({
|
return BlocBuilder<DashboardBloc, DashboardState>(
|
||||||
required IconData icon,
|
builder: (context, dashboardState) {
|
||||||
required String value,
|
if (dashboardState is DashboardLoading) {
|
||||||
required String title,
|
return const Center(
|
||||||
required Color color,
|
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
|
||||||
}) {
|
);
|
||||||
return Card(
|
}
|
||||||
elevation: 2,
|
|
||||||
child: Padding(
|
final dashboardData = (dashboardState is DashboardLoaded)
|
||||||
padding: const EdgeInsets.all(16),
|
? dashboardState.dashboardData
|
||||||
child: Column(
|
: null;
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
final stats = dashboardData?.stats;
|
||||||
children: [
|
|
||||||
Icon(icon, color: color, size: 32),
|
return SingleChildScrollView(
|
||||||
const SizedBox(height: 8),
|
padding: const EdgeInsets.all(24),
|
||||||
Text(
|
child: Column(
|
||||||
value,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(
|
children: [
|
||||||
fontSize: 24,
|
// En-tête
|
||||||
fontWeight: FontWeight.bold,
|
AnimatedFadeIn(
|
||||||
),
|
delay: const Duration(milliseconds: 100),
|
||||||
),
|
child: _buildUserHeader(user),
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
Text(
|
const SizedBox(height: 24),
|
||||||
title,
|
|
||||||
style: TextStyle(
|
// Balance principale (données backend réelles)
|
||||||
fontSize: 12,
|
AnimatedSlideIn(
|
||||||
color: Colors.grey[600],
|
delay: const Duration(milliseconds: 200),
|
||||||
),
|
child: UnionBalanceCard(
|
||||||
textAlign: TextAlign.center,
|
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({
|
PreferredSizeWidget _buildAppBar() {
|
||||||
required IconData icon,
|
return AppBar(
|
||||||
required String title,
|
backgroundColor: UnionFlowColors.surface,
|
||||||
required Color color,
|
elevation: 0,
|
||||||
required VoidCallback onTap,
|
title: Row(
|
||||||
}) {
|
children: [
|
||||||
return Card(
|
Container(
|
||||||
elevation: 2,
|
width: 32,
|
||||||
child: InkWell(
|
height: 32,
|
||||||
onTap: onTap,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
gradient: UnionFlowColors.primaryGradient,
|
||||||
child: Padding(
|
borderRadius: BorderRadius.circular(8),
|
||||||
padding: const EdgeInsets.all(16),
|
),
|
||||||
child: Column(
|
alignment: Alignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.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: 28),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
Text(
|
||||||
title,
|
'UnionFlow',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
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({
|
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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user