fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod

Auth:
- profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password

Multi-org (Phase 3):
- OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry
- org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role

Navigation:
- MorePage: navigation conditionnelle par typeOrganisation
- Suppression adaptive_navigation (remplacé par main_navigation_layout)

Auth AppAuth:
- keycloak_webview_auth_service: fixes AppAuth Android
- AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet

Onboarding:
- Nouveaux états: payment_method_page, onboarding_shared_widgets
- SouscriptionStatusModel mis à jour StatutValidationSouscription

Android:
- build.gradle: ProGuard/R8, network_security_config
- Gradle wrapper mis à jour
This commit is contained in:
dahoud
2026-04-07 20:56:03 +00:00
parent 22f9c7e9a1
commit 70cbd1c873
63 changed files with 9316 additions and 6122 deletions

View File

@@ -50,20 +50,23 @@ class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
// Écouter les events WebSocket
_webSocketEventSubscription = webSocketService.eventStream.listen(
(event) {
AppLogger.info('DashboardBloc: Event WebSocket reçu - ${event.eventType}');
try {
AppLogger.info('DashboardBloc: Event WebSocket reçu - ${event.eventType}');
// Dispatcher uniquement les events pertinents au dashboard
if (event is DashboardStatsEvent) {
add(RefreshDashboardFromWebSocket(event.data));
} else if (event is FinanceApprovalEvent) {
// Les approbations affectent les stats, rafraîchir
add(RefreshDashboardFromWebSocket(event.data));
} else if (event is MemberEvent) {
// Les changements de membres affectent les stats
add(RefreshDashboardFromWebSocket(event.data));
} else if (event is ContributionEvent) {
// Les cotisations affectent les stats financières
add(RefreshDashboardFromWebSocket(event.data));
if (isClosed) return;
// Dispatcher uniquement les events pertinents au dashboard
if (event is DashboardStatsEvent) {
add(RefreshDashboardFromWebSocket(event.data));
} else if (event is FinanceApprovalEvent) {
add(RefreshDashboardFromWebSocket(event.data));
} else if (event is MemberEvent) {
add(RefreshDashboardFromWebSocket(event.data));
} else if (event is ContributionEvent) {
add(RefreshDashboardFromWebSocket(event.data));
}
} catch (e, s) {
AppLogger.error('DashboardBloc: erreur lors du traitement WebSocket event', error: e);
}
},
onError: (error) {

View File

@@ -1,760 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../../shared/design_system/unionflow_design_v2.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../epargne/presentation/pages/epargne_page.dart';
import '../../../events/presentation/pages/events_page_wrapper.dart';
import '../bloc/dashboard_bloc.dart';
import '../../domain/entities/dashboard_entity.dart';
/// Page dashboard connectée au backend - Design UnionFlow Animé
class ConnectedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
const ConnectedDashboardPage({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<ConnectedDashboardPage> createState() => _ConnectedDashboardPageState();
}
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
PeriodFilter _selectedPeriod = PeriodFilter.month;
int _unreadNotifications = 5;
bool _isExporting = false;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.lightBackground,
appBar: _buildAppBar(),
body: AfricanPatternBackground(
child: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.primaryGreen),
);
}
if (state is DashboardMemberNotRegistered) {
return _buildMemberNotRegisteredState();
}
if (state is DashboardError) {
return _buildErrorState(state.message);
}
if (state is DashboardLoaded) {
return _buildDashboardContent(state);
}
return const SizedBox.shrink();
},
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: AppColors.lightSurface,
elevation: 0,
title: Row(
children: [
Hero(
tag: 'unionflow_logo',
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: const Text(
'U',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UnionFlow',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
Text(
'Dashboard',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: AppColors.textSecondaryLight,
),
),
],
),
],
),
automaticallyImplyLeading: false,
actions: [
UnionExportButton(
isLoading: _isExporting,
onExport: (exportType) {
showDialog(
context: context,
builder: (context) => ExportConfirmDialog(
exportType: exportType,
onConfirm: () => _handleExport(exportType),
),
);
},
),
const SizedBox(width: 8),
UnionNotificationBadge(
count: _unreadNotifications,
child: IconButton(
icon: const Icon(Icons.notifications_outlined),
color: AppColors.textPrimaryLight,
onPressed: () {
setState(() => _unreadNotifications = 0);
UnionNotificationToast.show(
context,
title: 'Notifications',
message: 'Aucune nouvelle notification',
icon: Icons.notifications_active,
color: UnionFlowColors.info,
);
},
),
),
const SizedBox(width: 8),
],
bottom: TabBar(
controller: _tabController,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicatorColor: AppColors.primaryGreen,
labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
tabs: const [
Tab(text: 'Vue d\'ensemble'),
Tab(text: 'Analytique'),
Tab(text: 'Activités'),
],
),
);
}
Widget _buildDashboardContent(DashboardLoaded state) {
final data = state.dashboardData;
return RefreshIndicator(
onRefresh: () async {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
color: AppColors.primaryGreen,
child: TabBarView(
controller: _tabController,
children: [
_buildOverviewTab(data),
_buildAnalyticsTab(data),
_buildActivitiesTab(data),
],
),
);
}
UnionTransactionTile _activityToTile(RecentActivityEntity a) {
final amount = a.metadata != null && a.metadata!['amount'] != null
? '${a.metadata!['amount']} FCFA'
: (a.title.isNotEmpty ? a.title : '-');
return UnionTransactionTile(
name: a.userName,
amount: amount,
status: a.type.isNotEmpty ? a.type : 'Confirmé',
date: a.timeAgo,
);
}
Widget _buildOverviewTab(DashboardEntity data) {
final stats = data.stats;
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Balance principale - Animée
AnimatedSlideIn(
delay: const Duration(milliseconds: 100),
child: UnionBalanceCard(
label: 'Caisse Totale',
amount: _formatAmount(stats.totalContributionAmount),
trend: stats.monthlyGrowth > 0 ? '+${(stats.monthlyGrowth * 100).toStringAsFixed(0)}% ce mois' : 'Stable',
isTrendPositive: true,
),
),
const SizedBox(height: 24),
// Stats en grille - Animées avec délai
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Membres',
value: stats.totalMembers.toString(),
icon: Icons.people_outline,
color: AppColors.primaryGreen,
trend: '+8%',
isTrendUp: true,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Actifs',
value: stats.activeMembers.toString(),
icon: Icons.check_circle_outline,
color: UnionFlowColors.success,
trend: '+5%',
isTrendUp: true,
),
),
],
),
),
const SizedBox(height: 12),
AnimatedSlideIn(
delay: const Duration(milliseconds: 300),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: stats.totalEvents.toString(),
icon: Icons.event_outlined,
color: UnionFlowColors.gold,
trend: '+3',
isTrendUp: true,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'À venir',
value: stats.upcomingEvents.toString(),
icon: Icons.calendar_today,
color: UnionFlowColors.amber,
),
),
],
),
),
const SizedBox(height: 12),
// Progression - Animée
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: UnionProgressCard(
title: 'Progression des Cotisations',
progress: 0.7,
subtitle: '70% des membres ont cotisé ce mois',
),
),
const SizedBox(height: 12),
// Actions rapides - Animées
AnimatedSlideIn(
delay: const Duration(milliseconds: 500),
begin: const Offset(0, 0.2),
child: UnionActionGrid(
actions: [
UnionActionButton(
icon: Icons.payment,
label: 'Cotiser',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
backgroundColor: UnionFlowColors.unionGreenPale,
iconColor: AppColors.primaryGreen,
),
UnionActionButton(
icon: Icons.send,
label: 'Envoyer',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
backgroundColor: UnionFlowColors.goldPale,
iconColor: UnionFlowColors.gold,
),
UnionActionButton(
icon: Icons.download,
label: 'Retirer',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const EpargnePage()),
);
},
backgroundColor: UnionFlowColors.terracottaPale,
iconColor: UnionFlowColors.terracotta,
),
UnionActionButton(
icon: Icons.add_circle_outline,
label: 'Créer',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()),
);
},
backgroundColor: UnionFlowColors.infoPale,
iconColor: UnionFlowColors.info,
),
],
),
),
const SizedBox(height: 12),
// Activité récente - Animée
AnimatedFadeIn(
delay: const Duration(milliseconds: 600),
child: UnionTransactionCard(
title: 'Activité Récente',
onSeeAll: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
transactions: data.recentActivities.take(6).map((a) => _activityToTile(a)).toList(),
),
),
],
),
);
}
Widget _buildAnalyticsTab(DashboardEntity data) {
final stats = data.stats;
final entrees = stats.totalContributionAmount;
final sorties = stats.pendingRequests * 1000.0;
final benefice = entrees - sorties;
final taux = (stats.engagementRate * 100).toStringAsFixed(0);
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filtre de période - Animé
AnimatedFadeIn(
delay: const Duration(milliseconds: 50),
child: UnionPeriodFilter(
selectedPeriod: _selectedPeriod,
onPeriodChanged: (period) {
setState(() => _selectedPeriod = period);
UnionNotificationToast.show(
context,
title: 'Période mise à jour',
message: 'Affichage pour ${period.label.toLowerCase()}',
icon: Icons.calendar_today,
color: AppColors.primaryGreen,
);
},
),
),
const SizedBox(height: 24),
// Line Chart - Animé (évolution basée sur total cotisations + croissance)
AnimatedSlideIn(
delay: const Duration(milliseconds: 100),
child: UnionLineChart(
title: 'Évolution de la Caisse',
subtitle: 'Derniers 12 mois',
spots: _buildEvolutionSpots(stats.totalContributionAmount, stats.monthlyGrowth),
),
),
const SizedBox(height: 24),
// Pie Chart - Animé
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: UnionPieChart(
title: 'Répartition des Cotisations',
subtitle: 'Par catégorie',
sections: [
UnionPieChartSection.create(
value: 40,
color: AppColors.primaryGreen,
title: '40%\nCotisations',
),
UnionPieChartSection.create(
value: 30,
color: UnionFlowColors.gold,
title: '30%\nÉpargne',
),
UnionPieChartSection.create(
value: 20,
color: UnionFlowColors.terracotta,
title: '20%\nSolidarité',
),
UnionPieChartSection.create(
value: 10,
color: UnionFlowColors.amber,
title: '10%\nAutres',
),
],
),
),
const SizedBox(height: 12),
// Titre
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: const Text(
'Métriques Financières',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
),
const SizedBox(height: 8),
// Métriques - Animées (données backend)
AnimatedSlideIn(
delay: const Duration(milliseconds: 500),
begin: const Offset(0, 0.2),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildFinanceMetric(
'Entrées',
_formatFcfa(entrees),
Icons.arrow_downward,
UnionFlowColors.success,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildFinanceMetric(
'Sorties',
_formatFcfa(sorties),
Icons.arrow_upward,
UnionFlowColors.error,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildFinanceMetric(
'Bénéfice',
_formatFcfa(benefice),
Icons.trending_up,
UnionFlowColors.gold,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildFinanceMetric(
'Taux',
'$taux%',
Icons.percent,
UnionFlowColors.info,
),
),
],
),
],
),
),
],
),
);
}
Widget _buildActivitiesTab(DashboardEntity data) {
final tiles = data.recentActivities.map((a) => _activityToTile(a)).toList();
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedSlideIn(
delay: const Duration(milliseconds: 100),
child: UnionTransactionCard(
title: 'Toutes les Activités',
onSeeAll: () {
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()),
);
},
transactions: tiles,
),
),
],
),
);
}
Future<void> _handleExport(ExportType exportType) async {
setState(() => _isExporting = true);
// Simulation de l'export (dans un vrai cas, appel API ici)
await Future.delayed(const Duration(seconds: 2));
setState(() => _isExporting = false);
if (mounted) {
UnionNotificationToast.show(
context,
title: 'Export réussi',
message: 'Le rapport ${exportType.label} a été généré avec succès',
icon: Icons.check_circle,
color: UnionFlowColors.success,
);
}
}
String _formatFcfa(double value) {
if (value >= 1000000) return '${(value / 1000000).toStringAsFixed(1)}M FCFA';
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}K FCFA';
return '${value.toStringAsFixed(0)} FCFA';
}
List<FlSpot> _buildEvolutionSpots(double totalAmount, double monthlyGrowth) {
final spots = <FlSpot>[];
var v = totalAmount * 0.5;
for (var i = 0; i < 12; i++) {
spots.add(FlSpot(i.toDouble(), v));
v = v * (1 + (monthlyGrowth > 0 ? monthlyGrowth : 0.02));
}
if (spots.isNotEmpty) spots[spots.length - 1] = FlSpot(11, totalAmount);
return spots;
}
Widget _buildFinanceMetric(String label, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.lightSurface,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 16, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textSecondaryLight,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: color,
),
),
],
),
),
],
),
);
}
Widget _buildMemberNotRegisteredState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
shape: BoxShape.circle,
),
child: const Icon(
Icons.person_add_alt_1_outlined,
size: 36,
color: Colors.white,
),
),
const SizedBox(height: 14),
const Text(
'Bienvenue dans UnionFlow',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: AppColors.textPrimaryLight,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Votre compte est en cours de configuration par un administrateur. '
'Votre tableau de bord sera disponible dès que votre profil membre aura été activé.',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondaryLight,
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.primaryGreen.withOpacity(0.3)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info_outline, size: 18, color: AppColors.primaryGreen),
SizedBox(width: 10),
Flexible(
child: Text(
'Contactez votre administrateur si ce message persiste.',
style: TextStyle(
fontSize: 13,
color: AppColors.primaryGreen,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
),
);
}
Widget _buildErrorState(String message) {
return Center(
child: AnimatedFadeIn(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
shape: BoxShape.circle,
),
child: const Icon(
Icons.error_outline,
size: 40,
color: UnionFlowColors.error,
),
),
const SizedBox(height: 12),
const Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.textPrimaryLight,
),
),
const SizedBox(height: 8),
Text(
message,
style: const TextStyle(
fontSize: 13,
color: AppColors.textSecondaryLight,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
UFPrimaryButton(
onPressed: () {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
label: 'RÉESSAYER',
),
],
),
),
);
}
String _formatAmount(num amount) {
return '${amount.toStringAsFixed(0).replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
)} FCFA';
}
}

View File

@@ -2,270 +2,210 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../shared/design_system/unionflow_design_v2.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../widgets/dashboard_drawer.dart';
/// Dashboard Visiteur - Design UnionFlow Version Publique
/// Dashboard affiché pour un compte authentifié sans rôle métier actif.
/// Cas typique : nouveau membre créé par un administrateur, en attente d'activation.
class VisitorDashboard extends StatelessWidget {
const VisitorDashboard({super.key});
@override
Widget build(BuildContext context) {
final authState = context.watch<AuthBloc>().state;
final email = authState is AuthAuthenticated ? authState.user.email : '';
final firstName = authState is AuthAuthenticated ? authState.user.firstName : '';
return Scaffold(
backgroundColor: UnionFlowColors.background,
appBar: _buildAppBar(),
drawer: DashboardDrawer(
onNavigate: (route) {
Navigator.of(context).pushNamed(route);
},
onLogout: () {
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
),
body: AfricanPatternBackground(
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message de bienvenue
AnimatedFadeIn(
delay: const Duration(milliseconds: 100),
child: _buildWelcomeCard(),
),
const SizedBox(height: 12),
appBar: _buildAppBar(context),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
// Fonctionnalités UnionFlow
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: const Text(
'Découvrez UnionFlow',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
// Icône centrale
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: UnionFlowColors.gold.withOpacity(0.12),
shape: BoxShape.circle,
),
child: const Icon(
Icons.hourglass_empty_rounded,
size: 40,
color: UnionFlowColors.gold,
),
),
const SizedBox(height: 20),
// Titre
Text(
firstName.isNotEmpty ? 'Bonjour $firstName,' : 'Bienvenue,',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: UnionFlowColors.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Votre compte est en attente d\'activation',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
// Email
if (email.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: UnionFlowColors.border),
),
),
const SizedBox(height: 8),
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: UnionStatWidget(
label: 'Organisations',
value: '500+',
icon: Icons.business_outlined,
color: UnionFlowColors.unionGreen,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Utilisateurs',
value: '10K+',
icon: Icons.people_outlined,
color: UnionFlowColors.gold,
const Icon(Icons.email_outlined, size: 14, color: UnionFlowColors.textSecondary),
const SizedBox(width: 6),
Text(
email,
style: const TextStyle(
fontSize: 13,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
const SizedBox(height: 12),
const SizedBox(height: 28),
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Transactions',
value: '1M+',
icon: Icons.payment_outlined,
color: UnionFlowColors.indigo,
),
// Message explicatif
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(12),
border: const Border(
left: BorderSide(color: UnionFlowColors.gold, width: 3),
),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Que se passe-t-il ?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Confiance',
value: '99%',
icon: Icons.verified_outlined,
color: UnionFlowColors.success,
),
),
],
),
),
const SizedBox(height: 12),
// Avantages
AnimatedSlideIn(
delay: const Duration(milliseconds: 500),
child: const Text(
'Nos Avantages',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 8),
AnimatedFadeIn(
delay: const Duration(milliseconds: 600),
child: _buildFeature(
'Gestion Simplifiée',
'Gérez vos cotisations, épargnes et crédits en un seul endroit',
Icons.dashboard_customize,
UnionFlowColors.unionGreen,
),
),
const SizedBox(height: 12),
AnimatedFadeIn(
delay: const Duration(milliseconds: 700),
child: _buildFeature(
'Sécurité Optimale',
'Vos données sont protégées avec un chiffrement de niveau bancaire',
Icons.security,
UnionFlowColors.indigo,
),
),
const SizedBox(height: 12),
AnimatedFadeIn(
delay: const Duration(milliseconds: 800),
child: _buildFeature(
'Solidarité Africaine',
'Entraide, tontines, mutuelles et coopératives à votre portée',
Icons.favorite_outline,
UnionFlowColors.terracotta,
),
),
const SizedBox(height: 12),
AnimatedFadeIn(
delay: const Duration(milliseconds: 900),
child: _buildFeature(
'Rapports Détaillés',
'Suivi en temps réel avec exports PDF, Excel et CSV',
Icons.analytics_outlined,
UnionFlowColors.gold,
),
),
const SizedBox(height: 16),
// Call to Action
AnimatedSlideIn(
delay: const Duration(milliseconds: 1000),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(12),
const SizedBox(height: 8),
_buildStep(
'1',
'Votre compte a été créé par l\'administrateur de votre organisation.',
UnionFlowColors.unionGreen,
),
child: Column(
children: [
const Icon(
Icons.rocket_launch,
size: 24,
color: Colors.white,
),
const SizedBox(height: 8),
const Text(
'Prêt à Commencer ?',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'Rejoignez des milliers d\'organisations qui nous font confiance',
style: TextStyle(
fontSize: 11,
color: Colors.white.withOpacity(0.9),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed('/login');
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: UnionFlowColors.unionGreen,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: const Text(
'Créer un Compte',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(height: 6),
TextButton(
onPressed: () {
Navigator.of(context).pushNamed('/login');
},
child: Text(
'Déjà membre ? Se connecter',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.white.withOpacity(0.9),
),
),
),
],
const SizedBox(height: 8),
_buildStep(
'2',
'Il doit être activé par un administrateur avant que vous puissiez accéder à la plateforme.',
UnionFlowColors.gold,
),
const SizedBox(height: 8),
_buildStep(
'3',
'Une fois activé, reconnectez-vous pour accéder à votre espace.',
UnionFlowColors.indigo,
),
],
),
),
const SizedBox(height: 20),
// Bouton actualiser
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
context.read<AuthBloc>().add(const AuthStatusChecked());
},
icon: const Icon(Icons.refresh_rounded, size: 18),
label: const Text(
'Vérifier l\'état de mon compte',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 0,
),
),
],
),
),
const SizedBox(height: 12),
// Bouton déconnexion
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
icon: const Icon(Icons.logout_rounded, size: 18),
label: const Text(
'Se déconnecter',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
style: OutlinedButton.styleFrom(
foregroundColor: UnionFlowColors.textSecondary,
side: const BorderSide(color: UnionFlowColors.border),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
),
const SizedBox(height: 32),
],
),
),
);
}
PreferredSizeWidget _buildAppBar() {
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
backgroundColor: UnionFlowColors.surface,
elevation: 0,
automaticallyImplyLeading: false,
title: Row(
children: [
Hero(
tag: 'unionflow_logo',
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: const Text(
'U',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
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,
),
),
),
@@ -282,7 +222,7 @@ class VisitorDashboard extends StatelessWidget {
),
),
Text(
'Découverte',
'Compte en attente',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
@@ -297,120 +237,39 @@ class VisitorDashboard extends StatelessWidget {
);
}
Widget _buildWelcomeCard() {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: UnionFlowColors.subtleGradient,
borderRadius: BorderRadius.circular(10),
border: const Border(
top: BorderSide(color: UnionFlowColors.unionGreen, width: 3),
),
boxShadow: UnionFlowColors.mediumShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.waving_hand,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bienvenue sur UnionFlow',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: UnionFlowColors.textPrimary,
),
),
SizedBox(height: 4),
Text(
'Votre plateforme de gestion mutualiste et associative',
style: TextStyle(
fontSize: 11,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
],
Widget _buildStep(String number, String text, Color color) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
const SizedBox(height: 8),
Text(
'Gérez vos mutuelles, tontines, coopératives et associations en toute simplicité. UnionFlow est la solution complète pour la solidarité africaine.',
style: TextStyle(
alignment: Alignment.center,
child: Text(
number,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 12,
height: 1.5,
color: UnionFlowColors.textPrimary.withOpacity(0.8),
color: UnionFlowColors.textSecondary,
),
),
],
),
);
}
Widget _buildFeature(String title, String description, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border(
left: BorderSide(color: color, width: 3),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 15),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
description,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
],
),
],
);
}
}

View File

@@ -1,416 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../pages/connected_dashboard_page.dart';
import '../../pages/advanced_dashboard_page.dart';
import '../../../../settings/presentation/pages/language_settings_page.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
/// Widget de navigation pour les différents types de dashboard
class DashboardNavigation extends StatefulWidget {
final String organizationId;
final String userId;
const DashboardNavigation({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<DashboardNavigation> createState() => _DashboardNavigationState();
}
class _DashboardNavigationState extends State<DashboardNavigation> {
int _currentIndex = 0;
final List<DashboardTab> _tabs = [
const DashboardTab(
title: 'Accueil',
icon: Icons.home,
activeIcon: Icons.home,
type: DashboardType.home,
),
const DashboardTab(
title: 'Analytics',
icon: Icons.analytics_outlined,
activeIcon: Icons.analytics,
type: DashboardType.analytics,
),
const DashboardTab(
title: 'Rapports',
icon: Icons.assessment_outlined,
activeIcon: Icons.assessment,
type: DashboardType.reports,
),
const DashboardTab(
title: 'Paramètres',
icon: Icons.settings_outlined,
activeIcon: Icons.settings,
type: DashboardType.settings,
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildCurrentPage(),
bottomNavigationBar: _buildBottomNavigationBar(),
floatingActionButton: _buildFloatingActionButton(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
Widget _buildCurrentPage() {
switch (_tabs[_currentIndex].type) {
case DashboardType.home:
return ConnectedDashboardPage(
organizationId: widget.organizationId,
userId: widget.userId,
);
case DashboardType.analytics:
return AdvancedDashboardPage(
organizationId: widget.organizationId,
userId: widget.userId,
);
case DashboardType.reports:
return _buildReportsPage();
case DashboardType.settings:
return _buildSettingsPage();
}
}
Widget _buildBottomNavigationBar() {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 8,
color: Theme.of(context).cardColor,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isActive = index == _currentIndex;
// Skip the middle item for FAB space
if (index == 2) {
return const SizedBox(width: 40);
}
return _buildNavItem(tab, isActive, index);
}).toList(),
),
),
),
);
}
Widget _buildNavItem(DashboardTab tab, bool isActive, int index) {
return GestureDetector(
onTap: () => setState(() => _currentIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isActive ? tab.activeIcon : tab.icon,
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
size: 20,
),
const SizedBox(height: 4),
Text(
tab.title,
style: AppTypography.badgeText.copyWith(
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
fontSize: 9,
),
),
],
),
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton(
onPressed: _showQuickActions,
backgroundColor: AppColors.primaryGreen,
elevation: 4,
child: const Icon(
Icons.add_outlined,
color: Colors.white,
size: 28,
),
);
}
Widget _buildReportsPage() {
return Scaffold(
appBar: AppBar(
title: Text('Rapports'.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.1)),
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.assessment_outlined,
size: 48,
color: AppColors.textSecondaryLight,
),
const SizedBox(height: 16),
Text(
'Page Rapports'.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'En cours de développement',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
),
),
);
}
Widget _buildSettingsPage() {
return Scaffold(
appBar: AppBar(
title: Text('Paramètres'.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.1)),
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSettingsSection(
'Apparence',
[
_buildSettingsTile(
'Thème',
'Design System UnionFlow',
Icons.palette_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Langue',
'Français',
Icons.language_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const LanguageSettingsPage())),
),
],
),
const SizedBox(height: 24),
_buildSettingsSection(
'Notifications',
[
_buildSettingsTile(
'Notifications push',
'Activées',
Icons.notifications_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Emails',
'Quotidien',
Icons.email_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
],
),
const SizedBox(height: 24),
_buildSettingsSection(
'Données',
[
_buildSettingsTile(
'Synchronisation',
'Automatique',
Icons.sync_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Cache',
'Vider le cache',
Icons.storage_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
],
),
],
),
);
}
Widget _buildSettingsSection(String title, List<Widget> children) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: AppColors.primaryGreen, fontSize: 10),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder),
),
child: Column(children: children),
),
],
);
}
Widget _buildSettingsTile(
String title,
String subtitle,
IconData icon,
VoidCallback onTap,
) {
return ListTile(
leading: Icon(icon, color: AppColors.primaryGreen, size: 20),
title: Text(title, style: AppTypography.actionText.copyWith(fontSize: 13)),
subtitle: Text(subtitle, style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
trailing: const Icon(
Icons.chevron_right_outlined,
color: AppColors.textSecondaryLight,
size: 16,
),
onTap: onTap,
);
}
void _showQuickActions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
Text(
'ACTIONS RAPIDES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
const SizedBox(height: 20),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: [
_buildQuickActionItem(context, 'Nouveau\nMembre', Icons.person_add_outlined, AppColors.success, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper()))),
_buildQuickActionItem(context, 'Créer\nÉvénement', Icons.event_available_outlined, AppColors.primaryGreen, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()))),
_buildQuickActionItem(context, 'Ajouter\nContribution', Icons.account_balance_wallet_outlined, AppColors.brandGreen, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()))),
_buildQuickActionItem(context, 'Générer\nRapport', Icons.assessment_outlined, AppColors.info, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper()))),
_buildQuickActionItem(context, 'Paramètres', Icons.settings_outlined, AppColors.textSecondaryLight, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage()))),
],
),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildQuickActionItem(BuildContext context, String title, IconData icon, Color color, VoidCallback onNavigate) {
return GestureDetector(
onTap: () {
Navigator.pop(context);
onNavigate();
},
child: Container(
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
title,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textPrimaryLight,
fontSize: 9,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class DashboardTab {
final String title;
final IconData icon;
final IconData activeIcon;
final DashboardType type;
const DashboardTab({
required this.title,
required this.icon,
required this.activeIcon,
required this.type,
});
}
enum DashboardType {
home,
analytics,
reports,
settings,
}