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:
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user