feat: WebSocket temps réel + Finance Workflow + corrections

- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics)
  * Backend: KafkaEventProducer, KafkaEventConsumer
  * Mobile: WebSocketService (reconnection, heartbeat, typed events)
  * DashboardBloc: Auto-refresh depuis WebSocket events

- Finance Workflow: approbations + budgets (backend + mobile)
  * Backend: entities, services, resources, migrations Flyway V6
  * Mobile: features finance_workflow complète avec BLoC

- Corrections DI: interfaces IRepository partout
  * IProfileRepository, IOrganizationRepository, IMembreRepository
  * GetIt configuré avec @injectable

- Spec-Kit: constitution + templates mis à jour
  * .specify/memory/constitution.md enrichie
  * Templates agent, plan, spec, tasks, checklist

- Nettoyage: fichiers temporaires supprimés

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -1,29 +1,83 @@
import 'dart:async';
import 'package:injectable/injectable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/entities/dashboard_entity.dart';
import '../../domain/usecases/get_dashboard_data.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/websocket/websocket_service.dart';
import '../../../../core/utils/logger.dart';
part 'dashboard_event.dart';
part 'dashboard_state.dart';
@injectable
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
final GetDashboardData getDashboardData;
final GetDashboardStats getDashboardStats;
final GetRecentActivities getRecentActivities;
final GetUpcomingEvents getUpcomingEvents;
final WebSocketService webSocketService;
StreamSubscription<WebSocketEvent>? _webSocketEventSubscription;
StreamSubscription<bool>? _webSocketConnectionSubscription;
DashboardBloc({
required this.getDashboardData,
required this.getDashboardStats,
required this.getRecentActivities,
required this.getUpcomingEvents,
required this.webSocketService,
}) : super(DashboardInitial()) {
on<LoadDashboardData>(_onLoadDashboardData);
on<RefreshDashboardData>(_onRefreshDashboardData);
on<LoadDashboardStats>(_onLoadDashboardStats);
on<LoadRecentActivities>(_onLoadRecentActivities);
on<LoadUpcomingEvents>(_onLoadUpcomingEvents);
on<RefreshDashboardFromWebSocket>(_onRefreshDashboardFromWebSocket);
on<WebSocketConnectionChanged>(_onWebSocketConnectionChanged);
// Initialiser WebSocket et écouter les events
_initializeWebSocket();
}
/// Initialise la connexion WebSocket et écoute les events
void _initializeWebSocket() {
// Connexion au WebSocket
webSocketService.connect();
AppLogger.info('DashboardBloc: WebSocket initialisé');
// Écouter les events WebSocket
_webSocketEventSubscription = webSocketService.eventStream.listen(
(event) {
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));
}
},
onError: (error) {
AppLogger.error('DashboardBloc: Erreur WebSocket', error: error);
},
);
// Écouter le statut de connexion WebSocket
_webSocketConnectionSubscription = webSocketService.connectionStatusStream.listen(
(isConnected) {
AppLogger.info('DashboardBloc: WebSocket ${isConnected ? "connecté" : "déconnecté"}');
add(WebSocketConnectionChanged(isConnected));
},
);
}
Future<void> _onLoadDashboardData(
@@ -161,6 +215,61 @@ class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
);
}
/// Rafraîchit le dashboard suite à un event WebSocket
Future<void> _onRefreshDashboardFromWebSocket(
RefreshDashboardFromWebSocket event,
Emitter<DashboardState> emit,
) async {
AppLogger.info('DashboardBloc: Rafraîchissement depuis WebSocket');
// Si le dashboard est chargé, on rafraîchit uniquement les stats
// pour éviter de recharger toutes les données
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
// Rafraîchir les stats depuis le backend
final result = await getDashboardStats(
GetDashboardStatsParams(
organizationId: currentData.organizationId,
userId: currentData.userId,
),
);
result.fold(
(failure) {
AppLogger.error('Erreur rafraîchissement stats WebSocket', error: failure);
// Ne pas émettre d'erreur, garder les données actuelles
},
(stats) {
final updatedData = DashboardEntity(
stats: stats,
recentActivities: currentData.recentActivities,
upcomingEvents: currentData.upcomingEvents,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
AppLogger.info('DashboardBloc: Stats rafraîchies depuis WebSocket');
},
);
}
}
/// Gère les changements de statut de connexion WebSocket
void _onWebSocketConnectionChanged(
WebSocketConnectionChanged event,
Emitter<DashboardState> emit,
) {
// Pour l'instant, on log juste le statut
// On pourrait ajouter un indicateur visuel dans l'UI plus tard
if (event.isConnected) {
AppLogger.info('DashboardBloc: WebSocket connecté - Temps réel actif');
} else {
AppLogger.warning('DashboardBloc: WebSocket déconnecté - Reconnexion en cours...');
}
}
String _mapFailureToMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
@@ -171,4 +280,18 @@ class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
return 'Une erreur inattendue s\'est produite.';
}
}
@override
Future<void> close() {
// Annuler les subscriptions WebSocket
_webSocketEventSubscription?.cancel();
_webSocketConnectionSubscription?.cancel();
// Déconnecter le WebSocket
webSocketService.disconnect();
AppLogger.info('DashboardBloc: Fermé et WebSocket déconnecté');
return super.close();
}
}

View File

@@ -75,3 +75,23 @@ class LoadUpcomingEvents extends DashboardEvent {
@override
List<Object> get props => [organizationId, userId, limit];
}
/// Event déclenché par WebSocket pour rafraîchir le dashboard
class RefreshDashboardFromWebSocket extends DashboardEvent {
final Map<String, dynamic> data;
const RefreshDashboardFromWebSocket(this.data);
@override
List<Object> get props => [data];
}
/// Event pour gérer les changements de statut WebSocket
class WebSocketConnectionChanged extends DashboardEvent {
final bool isConnected;
const WebSocketConnectionChanged(this.isConnected);
@override
List<Object> get props => [isConnected];
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../../data/repositories/finance_repository.dart';
import 'finance_event.dart';
import 'finance_state.dart';
@injectable
class FinanceBloc extends Bloc<FinanceEvent, FinanceState> {
final FinanceRepository _repository;
FinanceBloc(this._repository) : super(FinanceInitial()) {
on<LoadFinanceRequested>(_onLoadFinanceRequested);
on<FinancePaymentInitiated>(_onFinancePaymentInitiated);
}
Future<void> _onLoadFinanceRequested(LoadFinanceRequested event, Emitter<FinanceState> emit) async {
emit(FinanceLoading());
try {
final summary = await _repository.getFinancialSummary();
final transactions = await _repository.getTransactions();
emit(FinanceLoaded(summary: summary, transactions: transactions));
} catch (e) {
emit(FinanceError('Erreur chargement des finances: $e'));
}
}
void _onFinancePaymentInitiated(FinancePaymentInitiated event, Emitter<FinanceState> emit) {
// Intégration paiement: appeler le service Wave ou Orange Money (API paiement) selon le design métier.
// Pour l'instant, la transaction est gérée côté UI (payment_dialog) et le BLoC reste en FinanceLoaded.
if (state is FinanceLoaded) {
// Option: émettre FinancePaymentPending puis FinanceLoaded après confirmation API.
}
}
}

View File

@@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
abstract class FinanceEvent extends Equatable {
const FinanceEvent();
@override
List<Object> get props => [];
}
class LoadFinanceRequested extends FinanceEvent {}
class FinancePaymentInitiated extends FinanceEvent {
final String contributionId;
const FinancePaymentInitiated(this.contributionId);
@override
List<Object> get props => [contributionId];
}

View File

@@ -0,0 +1,67 @@
import 'package:equatable/equatable.dart';
class FinanceSummary extends Equatable {
final double totalContributionsPaid;
final double totalContributionsPending;
final double epargneBalance;
const FinanceSummary({
required this.totalContributionsPaid,
required this.totalContributionsPending,
required this.epargneBalance,
});
@override
List<Object?> get props => [totalContributionsPaid, totalContributionsPending, epargneBalance];
}
class FinanceTransaction extends Equatable {
final String id;
final String title;
final String date;
final double amount;
final String status;
const FinanceTransaction({
required this.id,
required this.title,
required this.date,
required this.amount,
required this.status, // "Payé", "En attente"
});
@override
List<Object?> get props => [id, title, date, amount, status];
}
abstract class FinanceState extends Equatable {
const FinanceState();
@override
List<Object?> get props => [];
}
class FinanceInitial extends FinanceState {}
class FinanceLoading extends FinanceState {}
class FinanceLoaded extends FinanceState {
final FinanceSummary summary;
final List<FinanceTransaction> transactions;
const FinanceLoaded({
required this.summary,
required this.transactions,
});
@override
List<Object?> get props => [summary, transactions];
}
class FinanceError extends FinanceState {
final String message;
const FinanceError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -1,14 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/dashboard_bloc.dart';
import '../widgets/connected/connected_stats_card.dart';
import '../widgets/connected/connected_recent_activities.dart';
import '../widgets/connected/connected_upcoming_events.dart';
import '../widgets/charts/dashboard_chart_widget.dart';
import '../widgets/metrics/real_time_metrics_widget.dart';
import '../widgets/notifications/dashboard_notifications_widget.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
import '../bloc/dashboard_bloc.dart';
import '../../domain/entities/dashboard_entity.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../../../../core/di/injection_container.dart';
import '../../../settings/presentation/pages/system_settings_page.dart';
/// Page dashboard avancée avec graphiques et analytics
class AdvancedDashboardPage extends StatefulWidget {
@@ -57,12 +60,12 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
return BlocProvider(
create: (context) => _dashboardBloc,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
_buildSliverAppBar(),
],
body: Column(
children: [
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: const UFAppBar(
title: 'DASHBOARD AVANCÉ',
),
body: Column(
children: [
_buildTabBar(),
Expanded(
child: TabBarView(
@@ -76,7 +79,6 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
),
],
),
),
floatingActionButton: _buildFloatingActionButton(),
),
);
@@ -89,10 +91,16 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: DashboardTheme.headerDecoration,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.primaryGreen, AppColors.brandGreen],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing20),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
@@ -100,34 +108,38 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.dashboard,
color: DashboardTheme.white,
Icons.dashboard_outlined,
color: Colors.white,
size: 32,
),
),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dashboard Avancé',
style: DashboardTheme.titleLarge.copyWith(
color: DashboardTheme.white,
fontSize: 28,
'DASHBOARD AVANCÉ',
style: AppTypography.headerSmall.copyWith(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 4),
Text(
'Analytics & Insights',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white.withOpacity(0.9),
'ANALYTICS & INSIGHTS',
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
letterSpacing: 1.1,
),
),
],
@@ -135,7 +147,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoaded || state is DashboardRefreshing) {
@@ -147,15 +159,15 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
_buildQuickStat(
'Membres',
'${data.stats.activeMembers}/${data.stats.totalMembers}',
Icons.people,
Icons.people_outline,
),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
_buildQuickStat(
'Événements',
'${data.stats.upcomingEvents}',
Icons.event,
Icons.event_outlined,
),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
_buildQuickStat(
'Croissance',
'${data.stats.monthlyGrowth.toStringAsFixed(1)}%',
@@ -177,17 +189,21 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
IconButton(
onPressed: _refreshDashboardData,
icon: const Icon(
Icons.refresh,
color: DashboardTheme.white,
Icons.refresh_outlined,
color: Colors.white,
),
),
IconButton(
onPressed: () {
// Navigation vers paramètres non encore connectée
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const SystemSettingsPage(),
),
);
},
icon: const Icon(
Icons.settings,
color: DashboardTheme.white,
Icons.settings_outlined,
color: Colors.white,
),
),
],
@@ -197,36 +213,39 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Widget _buildQuickStat(String label, String value, IconData icon) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing12,
vertical: DashboardTheme.spacing8,
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: DashboardTheme.white,
size: 16,
color: Colors.white,
size: 14,
),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white,
style: AppTypography.actionText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
Text(
label,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
label.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: 8,
letterSpacing: 0.5,
),
),
],
@@ -238,16 +257,21 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Widget _buildTabBar() {
return Container(
color: DashboardTheme.white,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(bottom: BorderSide(color: AppColors.lightBorder, width: 1)),
),
child: TabBar(
controller: _tabController,
labelColor: DashboardTheme.royalBlue,
unselectedLabelColor: DashboardTheme.grey500,
indicatorColor: DashboardTheme.royalBlue,
labelColor: AppColors.primaryGreen,
unselectedLabelColor: AppColors.textSecondaryLight,
indicatorColor: AppColors.primaryGreen,
indicatorWeight: 3,
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1),
tabs: const [
Tab(text: 'Vue d\'ensemble', icon: Icon(Icons.dashboard)),
Tab(text: 'Analytics', icon: Icon(Icons.analytics)),
Tab(text: 'Rapports', icon: Icon(Icons.assessment)),
Tab(text: 'VUE D\'ENSEMBLE'),
Tab(text: 'ANALYTICS'),
Tab(text: 'RAPPORTS'),
],
),
);
@@ -256,9 +280,9 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Widget _buildOverviewTab() {
return RefreshIndicator(
onRefresh: () async => _refreshDashboardData(),
color: DashboardTheme.royalBlue,
color: AppColors.primaryGreen,
child: SingleChildScrollView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Métriques temps réel
@@ -266,15 +290,15 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
organizationId: widget.organizationId,
userId: widget.userId,
),
const SizedBox(height: DashboardTheme.spacing24),
const SizedBox(height: 16),
// Grille de statistiques
_buildStatsGrid(),
const SizedBox(height: DashboardTheme.spacing24),
const SizedBox(height: 16),
// Notifications
const DashboardNotificationsWidget(maxNotifications: 3),
const SizedBox(height: DashboardTheme.spacing24),
const SizedBox(height: 16),
// Activités et événements
const Row(
@@ -282,7 +306,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Expanded(
child: ConnectedRecentActivities(maxItems: 3),
),
SizedBox(width: DashboardTheme.spacing16),
SizedBox(width: 16),
Expanded(
child: ConnectedUpcomingEvents(maxItems: 2),
),
@@ -296,7 +320,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Widget _buildAnalyticsTab() {
return const SingleChildScrollView(
padding: EdgeInsets.all(DashboardTheme.spacing16),
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
@@ -308,7 +332,7 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
height: 250,
),
),
SizedBox(width: DashboardTheme.spacing16),
SizedBox(width: 12),
Expanded(
child: DashboardChartWidget(
title: 'Croissance Mensuelle',
@@ -318,13 +342,13 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
),
],
),
SizedBox(height: DashboardTheme.spacing24),
SizedBox(height: 16),
DashboardChartWidget(
title: 'Tendance des Contributions',
chartType: DashboardChartType.contributionTrend,
height: 300,
),
SizedBox(height: DashboardTheme.spacing24),
SizedBox(height: 16),
DashboardChartWidget(
title: 'Participation aux Événements',
chartType: DashboardChartType.eventParticipation,
@@ -337,35 +361,35 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
Widget _buildReportsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildReportCard(
'Rapport Mensuel',
'Synthèse complète des activités du mois',
Icons.calendar_month,
DashboardTheme.royalBlue,
Icons.calendar_month_outlined,
AppColors.primaryGreen,
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 12),
_buildReportCard(
'Rapport Financier',
'État des contributions et finances',
Icons.account_balance,
DashboardTheme.tealBlue,
Icons.account_balance_wallet_outlined,
AppColors.success,
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 12),
_buildReportCard(
'Rapport d\'Activité',
'Analyse de l\'engagement des membres',
Icons.trending_up,
DashboardTheme.success,
AppColors.info,
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 12),
_buildReportCard(
'Rapport Événements',
'Statistiques des événements organisés',
Icons.event_note,
DashboardTheme.warning,
Icons.event_note_outlined,
AppColors.warning,
),
],
),
@@ -377,100 +401,77 @@ class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: DashboardTheme.spacing16,
mainAxisSpacing: DashboardTheme.spacing16,
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.25,
children: [
ConnectedStatsCard(
title: 'Membres totaux',
icon: Icons.people,
title: 'Membres',
icon: Icons.people_outline,
valueExtractor: (stats) => stats.totalMembers.toString(),
subtitleExtractor: (stats) => '${stats.activeMembers} actifs',
customColor: DashboardTheme.royalBlue,
),
ConnectedStatsCard(
title: 'Contributions',
icon: Icons.payment,
title: 'Finances',
icon: Icons.account_balance_wallet_outlined,
valueExtractor: (stats) => stats.formattedContributionAmount,
subtitleExtractor: (stats) => '${stats.totalContributions} versements',
customColor: DashboardTheme.tealBlue,
customColor: AppColors.success,
),
ConnectedStatsCard(
title: 'Événements',
icon: Icons.event,
icon: Icons.event_outlined,
valueExtractor: (stats) => stats.totalEvents.toString(),
subtitleExtractor: (stats) => '${stats.upcomingEvents} à venir',
customColor: DashboardTheme.success,
customColor: AppColors.info,
),
ConnectedStatsCard(
title: 'Engagement',
icon: Icons.favorite,
icon: Icons.star_outline,
valueExtractor: (stats) => '${(stats.engagementRate * 100).toStringAsFixed(0)}%',
subtitleExtractor: (stats) => stats.isHighEngagement ? 'Excellent' : 'Moyen',
customColor: DashboardTheme.warning,
subtitleExtractor: (stats) => stats.isHighEngagement ? 'Excellent' : 'Stable',
customColor: AppColors.warning,
),
],
);
}
Widget _buildReportCard(String title, String description, IconData icon, Color color) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
return CoreCard(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Icon(
icon,
color: color,
size: 24,
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.titleSmall,
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
description,
style: DashboardTheme.bodySmall,
),
Text(title, style: AppTypography.actionText.copyWith(fontSize: 12)),
Text(description, style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
],
),
),
IconButton(
onPressed: () {
// Génération de rapport non encore implémentée
},
icon: Icon(
Icons.download,
color: color,
),
),
const Icon(Icons.download_outlined, color: AppColors.textSecondaryLight, size: 18),
],
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton.extended(
return FloatingActionButton(
onPressed: () {
// Actions rapides non encore implémentées
// Actions rapides
},
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
icon: const Icon(Icons.add),
label: const Text('Action'),
backgroundColor: AppColors.primaryGreen,
child: const Icon(Icons.add, color: Colors.white),
);
}

View File

@@ -1,12 +1,15 @@
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 '../widgets/connected/connected_stats_card.dart';
import '../widgets/connected/connected_recent_activities.dart';
import '../widgets/connected/connected_upcoming_events.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
import '../../domain/entities/dashboard_entity.dart';
/// Page dashboard connectée au backend
/// Page dashboard connectée au backend - Design UnionFlow Animé
class ConnectedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
@@ -21,138 +24,662 @@ class ConnectedDashboardPage extends StatefulWidget {
State<ConnectedDashboardPage> createState() => _ConnectedDashboardPageState();
}
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> {
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();
// Charger les données du dashboard
_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: DashboardTheme.grey50,
appBar: AppBar(
title: const Text('Dashboard'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
elevation: 0,
backgroundColor: UnionFlowColors.background,
appBar: _buildAppBar(),
body: AfricanPatternBackground(
child: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
);
}
if (state is DashboardError) {
return _buildErrorState(state.message);
}
if (state is DashboardLoaded) {
return _buildDashboardContent(state);
}
return const SizedBox.shrink();
},
),
),
body: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(
color: DashboardTheme.royalBlue,
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: UnionFlowColors.surface,
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: UnionFlowColors.textPrimary,
),
),
Text(
'Dashboard',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary,
),
),
],
),
],
),
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: UnionFlowColors.textPrimary,
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: UnionFlowColors.unionGreen,
unselectedLabelColor: UnionFlowColors.textSecondary,
indicatorColor: UnionFlowColors.unionGreen,
labelStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
tabs: const [
Tab(text: 'Vue d\'ensemble'),
Tab(text: 'Analytique'),
Tab(text: 'Activités'),
],
),
);
}
if (state is DashboardError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: DashboardTheme.error,
Widget _buildDashboardContent(DashboardLoaded state) {
final data = state.dashboardData;
return RefreshIndicator(
onRefresh: () async {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
color: UnionFlowColors.unionGreen,
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(24),
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: UnionFlowColors.unionGreen,
trend: '+8%',
isTrendUp: true,
),
const SizedBox(height: DashboardTheme.spacing16),
const Text(
'Erreur de chargement',
style: DashboardTheme.titleMedium,
),
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: DashboardTheme.spacing8),
Text(
state.message,
style: DashboardTheme.bodyMedium,
textAlign: TextAlign.center,
),
],
),
),
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(height: DashboardTheme.spacing24),
ElevatedButton(
onPressed: () {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
style: ElevatedButton.styleFrom(
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'À venir',
value: stats.upcomingEvents.toString(),
icon: Icons.calendar_today,
color: UnionFlowColors.amber,
),
),
],
),
),
const SizedBox(height: 24),
// 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: 24),
// 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: UnionFlowColors.unionGreen,
),
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: 24),
// 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(24),
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: UnionFlowColors.unionGreen,
);
},
),
),
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: UnionFlowColors.unionGreen,
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: 24),
// Titre
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: const Text(
'Métriques Financières',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
// 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,
),
),
child: const Text('Réessayer'),
),
],
),
);
}
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,
),
),
],
),
],
),
),
],
),
);
}
if (state is DashboardLoaded) {
return RefreshIndicator(
onRefresh: () async {
Widget _buildActivitiesTab(DashboardEntity data) {
final tiles = data.recentActivities.map((a) => _activityToTile(a)).toList();
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(24),
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(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, size: 24, color: color),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textSecondary,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: color,
),
),
],
),
),
],
),
);
}
Widget _buildErrorState(String message) {
return Center(
child: AnimatedFadeIn(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
shape: BoxShape.circle,
),
child: const Icon(
Icons.error_outline,
size: 64,
color: UnionFlowColors.error,
),
),
const SizedBox(height: 24),
const Text(
'Erreur de chargement',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
message,
style: const TextStyle(
fontSize: 13,
color: UnionFlowColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
UFPrimaryButton(
onPressed: () {
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
color: DashboardTheme.royalBlue,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Statistiques
Row(
children: [
Expanded(
child: ConnectedStatsCard(
title: 'Membres',
icon: Icons.people,
valueExtractor: (stats) => stats.totalMembers.toString(),
subtitleExtractor: (stats) => '${stats.activeMembers} actifs',
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: ConnectedStatsCard(
title: 'Événements',
icon: Icons.event,
valueExtractor: (stats) => stats.totalEvents.toString(),
subtitleExtractor: (stats) => '${stats.upcomingEvents} à venir',
),
),
],
),
const SizedBox(height: DashboardTheme.spacing24),
// Activités récentes et événements à venir
const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ConnectedRecentActivities(),
),
SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: ConnectedUpcomingEvents(),
),
],
),
],
),
),
);
}
return const SizedBox.shrink();
},
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

@@ -6,9 +6,9 @@ import '../../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../../epargne/presentation/pages/epargne_page.dart';
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../../help/presentation/pages/help_support_page.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../widgets/dashboard_drawer.dart';
/// Dashboard Membre Actif - Design UnionFlow Enrichi
class ActiveMemberDashboard extends StatelessWidget {
@@ -19,6 +19,14 @@ class ActiveMemberDashboard extends StatelessWidget {
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: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
@@ -49,90 +57,115 @@ class ActiveMemberDashboard extends StatelessWidget {
),
const SizedBox(height: 24),
// Balance principale (données backend réelles)
// Balance principale ou Vue Unifiée (Compte Adhérent)
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: UnionBalanceCard(
label: 'Mon Solde Total',
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
trend: stats != null && stats.monthlyGrowth != 0
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
: 'Aucune variation',
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
),
child: dashboardData?.monCompte != null
? UnionUnifiedAccountCard(
numeroMembre: dashboardData!.monCompte!.numeroMembre,
organisationNom: dashboardData.monCompte!.organisationNom ?? 'UnionFlow',
soldeTotal: _formatAmount(dashboardData.monCompte!.soldeTotalDisponible),
capaciteEmprunt: _formatAmount(dashboardData.monCompte!.capaciteEmprunt),
epargneBloquee: _formatAmount(dashboardData.monCompte!.soldeBloque),
engagementRate: dashboardData.monCompte!.engagementRate,
)
: UnionBalanceCard(
label: 'Mon Solde Total',
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
trend: stats != null && stats.monthlyGrowth != 0
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
: 'Aucune variation',
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
),
),
const SizedBox(height: 24),
// Stats en grille (données backend réelles)
// Bloc KPI unifié (4 stats regroupées)
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Cotisations',
value: '${stats?.totalContributions ?? 0}',
icon: Icons.check_circle,
color: UnionFlowColors.success,
trend: stats != null && stats.monthlyGrowth > 0
? '+${stats.monthlyGrowth.toStringAsFixed(0)}%'
: null,
isTrendUp: (stats?.monthlyGrowth ?? 0) > 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: UnionFlowColors.border, width: 1),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
children: [
Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Cotisations',
value: '${stats?.totalContributions ?? 0}',
icon: Icons.check_circle,
color: (stats?.totalContributions ?? 0) > 0
? UnionFlowColors.success
: UnionFlowColors.textTertiary,
trend: stats != null && stats.totalContributions > 0 && stats.engagementRate > 0
? (stats.engagementRate >= 1.0
? 'Tout payé'
: '${(stats.engagementRate * 100).toStringAsFixed(0)}% payé')
: null,
isTrendUp: (stats?.engagementRate ?? 0) >= 1.0,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Engagement',
value: stats != null && stats.engagementRate > 0
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
: stats != null && stats.totalContributions > 0
? ''
: '0%',
icon: Icons.trending_up,
color: UnionFlowColors.gold,
trend: stats != null && stats.engagementRate > 0.9
? 'Excellent'
: stats != null && stats.engagementRate > 0.5
? 'Bon'
: null,
isTrendUp: (stats?.engagementRate ?? 0) > 0.7,
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Engagement',
value: stats != null
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
: '0%',
icon: Icons.trending_up,
color: UnionFlowColors.gold,
trend: stats != null && stats.engagementRate > 0.7
? 'Excellent'
: stats != null && stats.engagementRate > 0.5
? 'Bon'
: null,
isTrendUp: (stats?.engagementRate ?? 0) > 0.7,
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Contribution Totale',
value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0),
icon: Icons.savings,
color: UnionFlowColors.amber,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: '${stats?.upcomingEvents ?? 0}',
icon: Icons.event_available,
color: UnionFlowColors.terracotta,
),
),
],
),
),
],
),
),
const SizedBox(height: 12),
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Contribution Totale',
value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0),
icon: Icons.savings,
color: UnionFlowColors.amber,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: '${stats?.upcomingEvents ?? 0}',
icon: Icons.event_available,
color: UnionFlowColors.terracotta,
),
),
],
],
),
),
),
const SizedBox(height: 24),
// Activité récente (données backend)
if (dashboardData != null && dashboardData.hasRecentActivity) ...[
AnimatedFadeIn(
delay: const Duration(milliseconds: 500),
child: const Text(
const AnimatedFadeIn(
delay: Duration(milliseconds: 500),
child: Text(
'Activité Récente',
style: TextStyle(
fontSize: 16,
@@ -221,122 +254,128 @@ class ActiveMemberDashboard extends StatelessWidget {
const SizedBox(height: 24),
],
// Actions rapides
AnimatedFadeIn(
// Bloc Actions rapides unifié (6 boutons regroupés)
AnimatedSlideIn(
delay: const Duration(milliseconds: 700),
child: const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: UnionFlowColors.border, width: 1),
boxShadow: UnionFlowColors.softShadow,
),
),
),
const SizedBox(height: 16),
AnimatedSlideIn(
delay: const Duration(milliseconds: 800),
child: Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Cotiser',
icon: Icons.payment,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const CotisationsPageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.unionGreen,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Épargner',
icon: Icons.savings_outlined,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Cotiser',
icon: Icons.payment,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const CotisationsPageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.unionGreen,
),
);
},
backgroundColor: UnionFlowColors.gold,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Crédit',
icon: Icons.account_balance_wallet,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Épargner',
icon: Icons.savings_outlined,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
),
);
},
backgroundColor: UnionFlowColors.gold,
),
);
},
backgroundColor: UnionFlowColors.amber,
),
),
],
),
),
const SizedBox(height: 12),
AnimatedSlideIn(
delay: const Duration(milliseconds: 900),
child: Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Événements',
icon: Icons.event,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EventsPageWrapper(),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Crédit',
icon: Icons.account_balance_wallet,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
),
);
},
backgroundColor: UnionFlowColors.amber,
),
);
},
backgroundColor: UnionFlowColors.terracotta,
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Solidarité',
icon: Icons.favorite_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const DemandesAidePageWrapper(),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Événements',
icon: Icons.event,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EventsPageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.terracotta,
),
);
},
backgroundColor: UnionFlowColors.error,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Profil',
icon: Icons.person_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ProfilePageWrapper(),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Solidarité',
icon: Icons.favorite_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const DemandesAidePageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.error,
),
);
},
backgroundColor: UnionFlowColors.indigo,
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Profil',
icon: Icons.person_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ProfilePageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.indigo,
),
),
],
),
),
],
],
),
),
),
],
@@ -397,7 +436,7 @@ class ActiveMemberDashboard extends StatelessWidget {
),
],
),
automaticallyImplyLeading: false,
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
);
}

View File

@@ -1,68 +1,670 @@
/// Dashboard Modérateur - Management Hub Focalisé
/// Outils de modération et gestion partielle
library moderator_dashboard;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
import 'package:intl/intl.dart';
import '../../../../../shared/design_system/unionflow_design_v2.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../../epargne/presentation/pages/epargne_page.dart';
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../help/presentation/pages/help_support_page.dart';
import '../../widgets/dashboard_drawer.dart';
/// Dashboard Management Hub pour Modérateur
/// Dashboard Modérateur - Design UnionFlow pour Gestion Communauté
class ModeratorDashboard extends StatelessWidget {
const ModeratorDashboard({super.key});
String _formatAmount(double amount) {
if (amount >= 1000000) {
return '${(amount / 1000000).toStringAsFixed(amount % 1000000 == 0 ? 0 : 1)}M FCFA';
} else if (amount >= 1000) {
return '${(amount / 1000).toStringAsFixed(amount % 1000 == 0 ? 0 : 1)}K FCFA';
}
return '${amount.toStringAsFixed(0)} FCFA';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Modérateur
SliverAppBar(
expandedHeight: 160,
floating: false,
pinned: true,
backgroundColor: const Color(0xFFE17055), // Orange focus
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Management Hub',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFFE17055), Color(0xFFD63031)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
backgroundColor: UnionFlowColors.background,
appBar: _buildAppBar(),
drawer: DashboardDrawer(
onNavigate: (route) => Navigator.of(context).pushNamed(route),
onLogout: () => context.read<AuthBloc>().add(const AuthLogoutRequested()),
),
body: AfricanPatternBackground(
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
final user = (authState is AuthAuthenticated) ? authState.user : null;
return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, dashboardState) {
if (dashboardState is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
);
}
final dashboardData = (dashboardState is DashboardLoaded)
? dashboardState.dashboardData
: null;
final stats = dashboardData?.stats;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête Modérateur
AnimatedFadeIn(
delay: const Duration(milliseconds: 100),
child: _buildUserHeader(user),
),
const SizedBox(height: 24),
// Balance principale ou Vue Unifiée (Compte Adhérent)
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: dashboardData?.monCompte != null
? UnionUnifiedAccountCard(
numeroMembre: dashboardData!.monCompte!.numeroMembre,
organisationNom: dashboardData.monCompte!.organisationNom ?? 'UnionFlow',
soldeTotal: _formatAmount(dashboardData.monCompte!.soldeTotalDisponible),
capaciteEmprunt: _formatAmount(dashboardData.monCompte!.capaciteEmprunt),
epargneBloquee: _formatAmount(dashboardData.monCompte!.soldeBloque),
engagementRate: dashboardData.monCompte!.engagementRate,
)
: UnionBalanceCard(
label: 'Mon Solde Total',
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
trend: stats != null && stats.monthlyGrowth != 0
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
: 'Aucune variation',
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
),
),
const SizedBox(height: 24),
// Bloc KPI unifié (4 stats regroupées)
AnimatedFadeIn(
delay: const Duration(milliseconds: 250),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: UnionFlowColors.border, width: 1),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
children: [
Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Cotisations',
value: '${stats?.totalContributions ?? 0}',
icon: Icons.check_circle,
color: (stats?.totalContributions ?? 0) > 0
? UnionFlowColors.success
: UnionFlowColors.textTertiary,
trend: stats != null && stats.totalContributions > 0 && stats.engagementRate > 0
? (stats.engagementRate >= 1.0
? 'Tout payé'
: '${(stats.engagementRate * 100).toStringAsFixed(0)}% payé')
: null,
isTrendUp: (stats?.engagementRate ?? 0) >= 1.0,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Engagement',
value: stats != null && stats.engagementRate > 0
? '${(stats.engagementRate * 100).toStringAsFixed(0)}%'
: stats != null && stats.totalContributions > 0 ? '' : '0%',
icon: Icons.trending_up,
color: UnionFlowColors.gold,
trend: stats != null && stats.engagementRate > 0.9
? 'Excellent'
: stats != null && stats.engagementRate > 0.5 ? 'Bon' : null,
isTrendUp: (stats?.engagementRate ?? 0) > 0.7,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Contribution Totale',
value: _formatAmount(stats?.contributionsAmountOnly ?? stats?.totalContributionAmount ?? 0),
icon: Icons.savings,
color: UnionFlowColors.amber,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: '${stats?.upcomingEvents ?? 0}',
icon: Icons.event_available,
color: UnionFlowColors.terracotta,
),
),
],
),
],
),
),
),
const SizedBox(height: 24),
// Bloc Actions rapides unifié (6 boutons regroupés)
AnimatedSlideIn(
delay: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: UnionFlowColors.border, width: 1),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Cotiser',
icon: Icons.payment,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const CotisationsPageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.unionGreen,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Épargner',
icon: Icons.savings_outlined,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
),
);
},
backgroundColor: UnionFlowColors.gold,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Crédit',
icon: Icons.account_balance_wallet,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
),
);
},
backgroundColor: UnionFlowColors.amber,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Événements',
icon: Icons.event,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EventsPageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.terracotta,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Solidarité',
icon: Icons.favorite_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const DemandesAidePageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.error,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Profil',
icon: Icons.person_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ProfilePageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.indigo,
),
),
],
),
],
),
),
),
const SizedBox(height: 32),
// ——— Administration / Modération (tout en bas, après les actions membre) ———
AnimatedFadeIn(
delay: const Duration(milliseconds: 600),
child: const Text(
'Espace Modérateur',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
// Stats de modération (données backend réelles)
AnimatedSlideIn(
delay: const Duration(milliseconds: 600),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'En attente',
value: '${stats?.pendingRequests ?? 0}',
icon: Icons.pending_actions,
color: UnionFlowColors.warning,
trend: stats != null && stats.pendingRequests > 0 ? 'Action requise' : null,
isTrendUp: false,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Membres Actifs',
value: '${stats?.activeMembers ?? 0}',
icon: Icons.check_circle_outline,
color: UnionFlowColors.success,
trend: stats != null && stats.totalMembers > 0
? '${((stats.activeMembers / stats.totalMembers) * 100).toStringAsFixed(0)}%'
: null,
isTrendUp: true,
),
),
],
),
),
const SizedBox(height: 12),
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: '${stats?.upcomingEvents ?? 0}',
icon: Icons.event_outlined,
color: UnionFlowColors.gold,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Membres Total',
value: '${stats?.totalMembers ?? 0}',
icon: Icons.people_outline,
color: UnionFlowColors.unionGreen,
),
),
],
),
),
const SizedBox(height: 24),
// Activité des membres (données backend réelles)
if (stats != null && stats.totalMembers > 0)
AnimatedSlideIn(
delay: const Duration(milliseconds: 400),
child: UnionPieChart(
title: 'Activité des Membres',
subtitle: '${stats.totalMembers} membres au total',
sections: [
UnionPieChartSection.create(
value: stats.activeMembers.toDouble(),
color: UnionFlowColors.success,
title: '${((stats.activeMembers / stats.totalMembers) * 100).toStringAsFixed(0)}%\nActifs',
),
UnionPieChartSection.create(
value: (stats.totalMembers - stats.activeMembers).toDouble(),
color: UnionFlowColors.textTertiary,
title: '${(((stats.totalMembers - stats.activeMembers) / stats.totalMembers) * 100).toStringAsFixed(0)}%\nInactifs',
),
],
),
),
const SizedBox(height: 24),
// Demandes en attente (données backend)
if (dashboardData != null && dashboardData.hasRecentActivity) ...[
AnimatedFadeIn(
delay: const Duration(milliseconds: 500),
child: const Text(
'Activité Récente à Modérer',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
AnimatedSlideIn(
delay: const Duration(milliseconds: 600),
child: Column(
children: dashboardData.recentActivities.take(4).map((activity) =>
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: UnionFlowColors.border, width: 1),
boxShadow: UnionFlowColors.softShadow,
),
child: Row(
children: [
CircleAvatar(
radius: 22,
backgroundColor: UnionFlowColors.indigo.withOpacity(0.2),
child: Icon(
activity.type == 'member' ? Icons.person_add :
activity.type == 'event' ? Icons.event :
Icons.info_outline,
size: 20,
color: UnionFlowColors.indigo,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
activity.description,
style: const TextStyle(
fontSize: 11,
color: UnionFlowColors.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Text(
activity.timeAgo,
style: const TextStyle(
fontSize: 11,
color: UnionFlowColors.textTertiary,
),
),
],
),
)
).toList(),
),
),
],
const SizedBox(height: 24),
// Actions de modération
AnimatedSlideIn(
delay: const Duration(milliseconds: 700),
child: Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Approuver',
icon: Icons.check_circle,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
backgroundColor: UnionFlowColors.success,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Vérifier',
icon: Icons.visibility,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
backgroundColor: UnionFlowColors.info,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Signaler',
icon: Icons.flag,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const HelpSupportPage())),
backgroundColor: UnionFlowColors.error,
),
),
],
),
),
const SizedBox(height: 12),
AnimatedSlideIn(
delay: const Duration(milliseconds: 800),
child: Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Membres',
icon: Icons.people,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
backgroundColor: UnionFlowColors.unionGreen,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Contenus',
icon: Icons.article,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
backgroundColor: UnionFlowColors.gold,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Historique',
icon: Icons.history,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
backgroundColor: UnionFlowColors.indigo,
),
),
],
),
),
],
),
),
child: const Center(
child: Icon(Icons.manage_accounts, color: Colors.white, size: 60),
),
);
},
);
},
),
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: UnionFlowColors.surface,
elevation: 0,
title: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: const Text(
'U',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Métriques modération
_buildModerationMetrics(),
const SizedBox(height: SpacingTokens.xl),
// Actions modération
_buildModerationActions(),
const SizedBox(height: SpacingTokens.xl),
// Tâches en attente
_buildPendingTasks(),
const SizedBox(height: SpacingTokens.xl),
// Activité récente
_buildRecentActivity(),
],
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UnionFlow',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
Text(
'Modérateur',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary,
),
),
],
),
],
),
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
);
}
Widget _buildUserHeader(dynamic user) {
final year = user?.createdAt?.year ?? DateTime.now().year;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: UnionFlowColors.warmGradient,
borderRadius: BorderRadius.circular(16),
border: const Border(
top: BorderSide(color: UnionFlowColors.gold, width: 3),
),
boxShadow: UnionFlowColors.goldGlowShadow,
),
child: Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: Colors.white.withOpacity(0.3),
child: Text(
user?.initials ?? 'SM',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.fullName ?? 'Secrétaire',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'Depuis $year • Très Actif',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'ACTIF',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w800,
color: UnionFlowColors.gold,
letterSpacing: 0.5,
),
),
),
@@ -70,161 +672,4 @@ class ModeratorDashboard extends StatelessWidget {
),
);
}
Widget _buildModerationMetrics() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Métriques de Modération',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
stats: const [
DashboardStat(
icon: Icons.flag,
value: '12',
title: 'Signalements',
color: Color(0xFFE17055),
),
DashboardStat(
icon: Icons.pending_actions,
value: '8',
title: 'En Attente',
color: Color(0xFFD63031),
),
DashboardStat(
icon: Icons.check_circle,
value: '45',
title: 'Résolus',
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.people,
value: '156',
title: 'Membres',
color: Color(0xFF0984E3),
),
],
onStatTap: (type) {},
),
],
);
}
Widget _buildModerationActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions de Modération',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
children: [
DashboardQuickAction(
icon: Icons.gavel,
title: 'Modérer',
color: const Color(0xFFE17055),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.person_remove,
title: 'Suspendre',
color: const Color(0xFFD63031),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.message,
title: 'Communiquer',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.report,
title: 'Rapport',
color: const Color(0xFF6C5CE7),
onTap: () {},
),
],
),
],
);
}
Widget _buildPendingTasks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Tâches en Attente',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Column(
children: [
ListTile(
leading: CircleAvatar(
backgroundColor: Color(0xFFFFE0E0),
child: Icon(Icons.flag, color: Color(0xFFD63031)),
),
title: Text('Contenu inapproprié signalé'),
subtitle: Text('Commentaire sur événement'),
trailing: Text('Urgent'),
),
Divider(height: 1),
ListTile(
leading: CircleAvatar(
backgroundColor: Color(0xFFFFF3E0),
child: Icon(Icons.person_add, color: Color(0xFFE17055)),
),
title: Text('Demande d\'adhésion'),
subtitle: Text('Marie Dubois'),
trailing: Text('2j'),
),
],
),
),
],
);
}
Widget _buildRecentActivity() {
return const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Signalement traité',
subtitle: 'Contenu supprimé',
icon: Icons.check_circle,
color: Color(0xFF00B894),
time: 'Il y a 1h',
),
DashboardActivity(
title: 'Membre suspendu',
subtitle: 'Violation des règles',
icon: Icons.person_remove,
color: Color(0xFFD63031),
time: 'Il y a 3h',
),
],
);
}
}

View File

@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../shared/design_system/unionflow_design_v2.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../organizations/data/models/organization_model.dart';
import '../../../../organizations/data/services/organization_service.dart';
import 'org_admin_dashboard.dart';
/// Charge l'organisation du membre connecté (GET /api/organisations/mes) puis
/// affiche le dashboard admin avec les données backend pour cette organisation.
class OrgAdminDashboardLoader extends StatelessWidget {
const OrgAdminDashboardLoader({
super.key,
required this.userId,
});
final String userId;
@override
Widget build(BuildContext context) {
return FutureBuilder<List<OrganizationModel>>(
future: getIt<OrganizationService>().getMesOrganisations(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: const Center(
child: CircularProgressIndicator(color: UnionFlowColors.gold),
),
);
}
if (snapshot.hasError) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: UnionFlowColors.error),
const SizedBox(height: 16),
Text(
'Impossible de charger votre organisation',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'${snapshot.error}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
),
);
}
final orgs = snapshot.data ?? [];
final orgsWithId = orgs.where((o) => o.id != null && o.id!.isNotEmpty).toList();
if (orgsWithId.isEmpty) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
orgs.isEmpty
? 'Aucune organisation associée à votre compte.'
: 'Aucune organisation valide (id manquant).',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: UnionFlowColors.textSecondary,
),
),
),
),
);
}
final firstOrgId = orgsWithId.first.id!;
return BlocProvider<DashboardBloc>(
create: (context) => getIt<DashboardBloc>()
..add(LoadDashboardData(
organizationId: firstOrgId,
userId: userId,
)),
child: const OrgAdminDashboard(),
);
},
);
}
}

View File

@@ -6,6 +6,8 @@ library role_dashboards;
export 'super_admin_dashboard.dart';
export 'org_admin_dashboard.dart';
export 'moderator_dashboard.dart';
export 'consultant_dashboard.dart';
export 'hr_manager_dashboard.dart';
export 'active_member_dashboard.dart';
export 'simple_member_dashboard.dart';
export 'visitor_dashboard.dart';

View File

@@ -1,360 +1,436 @@
/// Dashboard Membre Simple - Personal Space Minimaliste
/// Interface simplifiée pour accès basique
library simple_member_dashboard;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
import '../../../../../shared/design_system/unionflow_design_v2.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../../epargne/presentation/pages/epargne_page.dart';
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../../help/presentation/pages/help_support_page.dart';
import '../../widgets/dashboard_drawer.dart';
/// Dashboard Personal Space pour Membre Simple
/// Dashboard Membre Simple - Design UnionFlow
class SimpleMemberDashboard extends StatelessWidget {
const SimpleMemberDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Membre Simple
SliverAppBar(
expandedHeight: 140,
floating: false,
pinned: true,
backgroundColor: const Color(0xFF00CEC9), // Teal simple
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Mon Espace',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF00CEC9), Color(0xFF00B3B3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
backgroundColor: UnionFlowColors.background,
appBar: _buildAppBar(),
drawer: DashboardDrawer(
onNavigate: (route) {
Navigator.of(context).pushNamed(route);
},
onLogout: () {
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
),
body: AfricanPatternBackground(
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
final user = (authState is AuthAuthenticated) ? authState.user : null;
return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, dashboardState) {
if (dashboardState is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(color: UnionFlowColors.unionGreen),
);
}
final dashboardData = (dashboardState is DashboardLoaded)
? dashboardState.dashboardData
: null;
final stats = dashboardData?.stats;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec badge de rôle
AnimatedFadeIn(
delay: const Duration(milliseconds: 100),
child: _buildUserHeader(user),
),
const SizedBox(height: 24),
// Solde personnel
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: UnionBalanceCard(
label: 'Mon Solde',
amount: _formatAmount(stats?.totalContributionAmount ?? 0),
trend: stats != null && stats.monthlyGrowth != 0
? '${stats.monthlyGrowth > 0 ? '+' : ''}${stats.monthlyGrowth.toStringAsFixed(1)}% ce mois'
: 'Aucune variation',
isTrendPositive: (stats?.monthlyGrowth ?? 0) >= 0,
),
),
const SizedBox(height: 24),
// Ma situation
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: const Text(
'Ma Situation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
AnimatedSlideIn(
delay: const Duration(milliseconds: 400),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Cotisations',
value: stats != null && stats.totalContributions > 0 ? 'À jour' : 'En retard',
icon: Icons.check_circle_outline,
color: stats != null && stats.totalContributions > 0
? UnionFlowColors.success
: UnionFlowColors.warning,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Événements',
value: '${stats?.upcomingEvents ?? 0}',
icon: Icons.event_outlined,
color: UnionFlowColors.gold,
),
),
],
),
),
const SizedBox(height: 24),
// Actions rapides
AnimatedFadeIn(
delay: const Duration(milliseconds: 500),
child: const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
AnimatedSlideIn(
delay: const Duration(milliseconds: 600),
child: Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Cotiser',
icon: Icons.payment,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const CotisationsPageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.unionGreen,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Épargner',
icon: Icons.savings_outlined,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const EpargnePage(),
),
);
},
backgroundColor: UnionFlowColors.gold,
),
),
],
),
),
const SizedBox(height: 12),
AnimatedSlideIn(
delay: const Duration(milliseconds: 700),
child: Row(
children: [
Expanded(
child: UnionActionButton(
label: 'Mes Infos',
icon: Icons.person_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ProfilePageWrapper(),
),
);
},
backgroundColor: UnionFlowColors.indigo,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionActionButton(
label: 'Support',
icon: Icons.help_outline,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const HelpSupportPage(),
),
);
},
backgroundColor: UnionFlowColors.terracotta,
),
),
],
),
),
const SizedBox(height: 24),
// Événements à venir (données backend)
if (dashboardData != null && dashboardData.hasUpcomingEvents) ...[
AnimatedFadeIn(
delay: const Duration(milliseconds: 800),
child: const Text(
'Événements à Venir',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
AnimatedSlideIn(
delay: const Duration(milliseconds: 900),
child: Column(
children: dashboardData.upcomingEvents.take(2).map((event) =>
Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: UnionFlowColors.border, width: 1),
boxShadow: UnionFlowColors.softShadow,
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: UnionFlowColors.gold.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.event,
color: UnionFlowColors.gold,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: UnionFlowColors.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
event.formattedDate,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
if (event.daysUntilEventInt <= 7)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: UnionFlowColors.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${event.daysUntilEventInt}j',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: UnionFlowColors.warning,
),
),
),
],
),
)
).toList(),
),
),
],
],
),
),
child: const Center(
child: Icon(Icons.person, color: Colors.white, size: 50),
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Profil personnel
_buildPersonalProfile(),
const SizedBox(height: SpacingTokens.xl),
// Mes informations
_buildMyInfo(),
const SizedBox(height: SpacingTokens.xl),
// Actions simples
_buildSimpleActions(),
const SizedBox(height: SpacingTokens.xl),
// Événements publics
_buildPublicEvents(),
const SizedBox(height: SpacingTokens.xl),
// Mon historique
_buildMyHistory(),
],
),
),
),
],
);
},
);
},
),
),
);
}
Widget _buildPersonalProfile() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: UnionFlowColors.surface,
elevation: 0,
title: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: const Text(
'U',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 18,
),
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UnionFlow',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
Text(
'Membre Simple',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary,
),
),
],
),
],
),
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
);
}
Widget _buildUserHeader(dynamic user) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: UnionFlowColors.subtleGradient,
borderRadius: BorderRadius.circular(16),
border: const Border(
top: BorderSide(color: UnionFlowColors.unionGreen, width: 3),
),
boxShadow: UnionFlowColors.softShadow,
),
child: Row(
children: [
const CircleAvatar(
radius: 35,
backgroundColor: Color(0xFF00CEC9),
child: Icon(Icons.person, color: Colors.white, size: 35),
CircleAvatar(
radius: 28,
backgroundColor: UnionFlowColors.unionGreen.withOpacity(0.2),
child: Text(
user?.initials ?? 'M',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: UnionFlowColors.unionGreen,
),
),
),
const SizedBox(width: SpacingTokens.md),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pierre Dupont',
style: TypographyTokens.headlineMedium.copyWith(
fontWeight: FontWeight.bold,
user?.fullName ?? 'Membre',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Membre depuis 6 mois',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.textSecondary,
),
),
const SizedBox(height: SpacingTokens.sm),
Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: Text(
'Membre Simple',
style: TypographyTokens.bodySmall.copyWith(
color: const Color(0xFF00CEC9),
fontWeight: FontWeight.w600,
),
'Membre depuis ${user?.createdAt.year ?? 2024}',
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreen,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'MEMBRE',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.5,
),
),
),
],
),
);
}
Widget _buildMyInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mes Informations',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
const DashboardStatsGrid(
stats: [
DashboardStat(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.event,
value: '2',
title: 'Événements',
color: Color(0xFF00CEC9),
),
DashboardStat(
icon: Icons.account_circle,
value: '100%',
title: 'Profil',
color: Color(0xFF0984E3),
),
DashboardStat(
icon: Icons.notifications,
value: '3',
title: 'Notifications',
color: Color(0xFFE17055),
),
],
),
],
);
}
Widget _buildSimpleActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Actions Disponibles',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
children: [
DashboardQuickAction(
icon: Icons.edit,
title: 'Modifier Profil',
color: const Color(0xFF00CEC9),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.payment,
title: 'Mes Cotisations',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.event,
title: 'Événements',
color: const Color(0xFF00B894),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.help,
title: 'Aide',
color: const Color(0xFFE17055),
onTap: () {},
),
],
),
],
);
}
Widget _buildPublicEvents() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Événements Disponibles',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Icon(
Icons.event,
color: Color(0xFF00B894),
),
),
title: const Text('Assemblée Générale'),
subtitle: const Text('15 décembre • 19h00'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'Public',
style: TextStyle(
color: Color(0xFF00B894),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
const Divider(height: 1),
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Icon(
Icons.celebration,
color: Color(0xFF00CEC9),
),
),
title: const Text('Soirée de Noël'),
subtitle: const Text('22 décembre • 20h00'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00CEC9).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'Public',
style: TextStyle(
color: Color(0xFF00CEC9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
],
);
}
Widget _buildMyHistory() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mon Historique',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Cotisation payée',
subtitle: 'Décembre 2024',
icon: Icons.payment,
color: Color(0xFF00B894),
time: 'Il y a 1j',
),
DashboardActivity(
title: 'Profil mis à jour',
subtitle: 'Informations personnelles',
icon: Icons.edit,
color: Color(0xFF00CEC9),
time: 'Il y a 1 sem',
),
DashboardActivity(
title: 'Inscription événement',
subtitle: 'Assemblée Générale',
icon: Icons.event,
color: Color(0xFF0984E3),
time: 'Il y a 2 sem',
),
],
),
],
);
String _formatAmount(double amount) {
if (amount >= 1000000) {
return '${(amount / 1000000).toStringAsFixed(1)}M FCFA';
} else if (amount >= 1000) {
return '${(amount / 1000).toStringAsFixed(0)}K FCFA';
}
return '${amount.toStringAsFixed(0)} FCFA';
}
}

View File

@@ -1,156 +1,364 @@
/// Dashboard Visiteur - Landing Experience Accueillante
/// Interface publique pour découvrir l'organisation
library visitor_dashboard;
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/tokens/color_tokens.dart';
import '../../../../../shared/design_system/tokens/radius_tokens.dart';
import '../../../../../shared/design_system/tokens/spacing_tokens.dart';
import '../../../../../shared/design_system/tokens/typography_tokens.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 Landing Experience pour Visiteur
/// Dashboard Visiteur - Design UnionFlow Version Publique
class VisitorDashboard extends StatelessWidget {
const VisitorDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: ColorTokens.surface,
body: CustomScrollView(
slivers: [
// App Bar Visiteur
SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
backgroundColor: const Color(0xFF6C5CE7), // Indigo accueillant
flexibleSpace: FlexibleSpaceBar(
title: const Text(
'Découvrir UnionFlow',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
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(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message de bienvenue
AnimatedFadeIn(
delay: const Duration(milliseconds: 100),
child: _buildWelcomeCard(),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
const SizedBox(height: 24),
// Fonctionnalités UnionFlow
AnimatedSlideIn(
delay: const Duration(milliseconds: 200),
child: const Text(
'Découvrez UnionFlow',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
child: Stack(
),
const SizedBox(height: 16),
AnimatedFadeIn(
delay: const Duration(milliseconds: 300),
child: Row(
children: [
// Motif d'accueil
Positioned.fill(
child: CustomPaint(
painter: _WelcomePatternPainter(),
Expanded(
child: UnionStatWidget(
label: 'Organisations',
value: '500+',
icon: Icons.business_outlined,
color: UnionFlowColors.unionGreen,
),
),
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.waving_hand, color: Colors.white, size: 60),
SizedBox(height: 8),
Text(
'Bienvenue !',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Utilisateurs',
value: '10K+',
icon: Icons.people_outlined,
color: UnionFlowColors.gold,
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message d'accueil
_buildWelcomeMessage(),
const SizedBox(height: SpacingTokens.xl),
// À propos de l'organisation
_buildAboutOrganization(),
const SizedBox(height: SpacingTokens.xl),
// Événements publics
_buildPublicEvents(),
const SizedBox(height: SpacingTokens.xl),
// Comment rejoindre
_buildHowToJoin(),
const SizedBox(height: SpacingTokens.xl),
// Contact
_buildContactInfo(),
],
const SizedBox(height: 12),
AnimatedFadeIn(
delay: const Duration(milliseconds: 400),
child: Row(
children: [
Expanded(
child: UnionStatWidget(
label: 'Transactions',
value: '1M+',
icon: Icons.payment_outlined,
color: UnionFlowColors.indigo,
),
),
const SizedBox(width: 12),
Expanded(
child: UnionStatWidget(
label: 'Confiance',
value: '99%',
icon: Icons.verified_outlined,
color: UnionFlowColors.success,
),
),
],
),
),
),
const SizedBox(height: 24),
// Avantages
AnimatedSlideIn(
delay: const Duration(milliseconds: 500),
child: const Text(
'Nos Avantages',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
),
const SizedBox(height: 16),
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: 32),
// Call to Action
AnimatedSlideIn(
delay: const Duration(milliseconds: 1000),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: UnionFlowColors.greenGlowShadow,
),
child: Column(
children: [
const Icon(
Icons.rocket_launch,
size: 48,
color: Colors.white,
),
const SizedBox(height: 16),
const Text(
'Prêt à Commencer ?',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Rejoignez des milliers d\'organisations qui nous font confiance',
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(0.9),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
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: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Text(
'Créer un Compte',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () {
Navigator.of(context).pushNamed('/login');
},
child: Text(
'Déjà membre ? Se connecter',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.white.withOpacity(0.9),
),
),
),
],
),
),
),
],
),
],
),
),
);
}
Widget _buildWelcomeMessage() {
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: UnionFlowColors.surface,
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: UnionFlowColors.textPrimary,
),
),
Text(
'Découverte',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: UnionFlowColors.textSecondary,
),
),
],
),
],
),
iconTheme: const IconThemeData(color: UnionFlowColors.textPrimary),
);
}
Widget _buildWelcomeCard() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
gradient: UnionFlowColors.subtleGradient,
borderRadius: BorderRadius.circular(20),
border: const Border(
top: BorderSide(color: UnionFlowColors.unionGreen, width: 3),
),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
boxShadow: UnionFlowColors.mediumShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.info_outline, color: Colors.white, size: 30),
const SizedBox(width: SpacingTokens.sm),
Expanded(
child: Text(
'Découvrez notre communauté',
style: TypographyTokens.headlineMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.waving_hand,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bienvenue sur UnionFlow',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: UnionFlowColors.textPrimary,
),
),
SizedBox(height: 4),
Text(
'Votre plateforme de gestion mutualiste et associative',
style: TextStyle(
fontSize: 13,
color: UnionFlowColors.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: SpacingTokens.md),
const SizedBox(height: 16),
Text(
'Bienvenue sur UnionFlow ! Explorez notre organisation, découvrez nos événements publics et apprenez comment nous rejoindre.',
style: TypographyTokens.bodyLarge.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: SpacingTokens.md),
ElevatedButton(
onPressed: () => _onJoinNow(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF6C5CE7),
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.lg,
vertical: SpacingTokens.md,
),
),
child: const Text(
'Nous Rejoindre',
style: TextStyle(fontWeight: FontWeight.bold),
'Gérez vos mutuelles, tontines, coopératives et associations en toute simplicité. UnionFlow est la solution complète pour la solidarité africaine.',
style: TextStyle(
fontSize: 14,
height: 1.5,
color: UnionFlowColors.textPrimary.withOpacity(0.8),
),
),
],
@@ -158,397 +366,53 @@ class VisitorDashboard extends StatelessWidget {
);
}
Widget _buildAboutOrganization() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'À Propos de Nous',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
Widget _buildFeature(String title, String description, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border(
left: BorderSide(color: color, width: 4),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
boxShadow: UnionFlowColors.softShadow,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 24),
),
child: Column(
children: [
Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.business,
color: Color(0xFF6C5CE7),
size: 30,
),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Association des Développeurs',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Communauté tech passionnée',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: SpacingTokens.md),
const Text(
'Nous sommes une association dynamique qui rassemble les passionnés de technologie. Notre mission est de favoriser l\'apprentissage, le partage de connaissances et l\'entraide dans le domaine du développement.',
style: TypographyTokens.bodyMedium,
),
const SizedBox(height: SpacingTokens.md),
// Statistiques publiques
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildPublicStat('156', 'Membres'),
_buildPublicStat('24', 'Événements/an'),
_buildPublicStat('5', 'Ans d\'existence'),
],
),
],
),
),
],
);
}
Widget _buildPublicStat(String value, String label) {
return Column(
children: [
Text(
value,
style: TypographyTokens.headlineMedium.copyWith(
color: const Color(0xFF6C5CE7),
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.textSecondary,
),
),
],
);
}
Widget _buildPublicEvents() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Événements Publics',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('15', style: TextStyle(fontWeight: FontWeight.bold)),
Text('DÉC', style: TextStyle(fontSize: 10)),
],
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: UnionFlowColors.textPrimary,
),
),
title: const Text('Assemblée Générale Publique'),
subtitle: const Text('Salle communale • 19h00 • Gratuit'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF00B894).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'OUVERT',
style: TextStyle(
color: Color(0xFF00B894),
fontSize: 10,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 4),
Text(
description,
style: const TextStyle(
fontSize: 12,
color: UnionFlowColors.textSecondary,
),
),
),
const Divider(height: 1),
ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(25),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('20', style: TextStyle(fontWeight: FontWeight.bold)),
Text('DÉC', style: TextStyle(fontSize: 10)),
],
),
),
title: const Text('Conférence Tech Trends 2025'),
subtitle: const Text('Amphithéâtre Université • 14h00 • Gratuit'),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7).withOpacity(0.1),
borderRadius: BorderRadius.circular(RadiusTokens.sm),
),
child: const Text(
'OUVERT',
style: TextStyle(
color: Color(0xFF6C5CE7),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
);
}
Widget _buildHowToJoin() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comment Nous Rejoindre',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildJoinStep('1', 'Créer un compte', 'Inscription gratuite en 2 minutes'),
const SizedBox(height: SpacingTokens.md),
_buildJoinStep('2', 'Compléter le profil', 'Partagez vos centres d\'intérêt'),
const SizedBox(height: SpacingTokens.md),
_buildJoinStep('3', 'Validation', 'Approbation par nos modérateurs'),
const SizedBox(height: SpacingTokens.lg),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _onStartRegistration(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.md),
),
child: const Text(
'Commencer l\'inscription',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
);
}
Widget _buildJoinStep(String number, String title, String description) {
return Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF6C5CE7),
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: Text(
number,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
],
),
),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
description,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.textSecondary,
),
),
],
),
),
],
],
),
);
}
Widget _buildContactInfo() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nous Contacter',
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(RadiusTokens.md),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Column(
children: [
ListTile(
leading: Icon(Icons.email, color: Color(0xFF6C5CE7)),
title: Text('Email'),
subtitle: Text('contact@association-dev.fr'),
contentPadding: EdgeInsets.zero,
),
ListTile(
leading: Icon(Icons.phone, color: Color(0xFF6C5CE7)),
title: Text('Téléphone'),
subtitle: Text('+33 1 23 45 67 89'),
contentPadding: EdgeInsets.zero,
),
ListTile(
leading: Icon(Icons.location_on, color: Color(0xFF6C5CE7)),
title: Text('Adresse'),
subtitle: Text('123 Rue de la Tech, 75001 Paris'),
contentPadding: EdgeInsets.zero,
),
],
),
),
],
);
}
// === CALLBACKS ===
void _onJoinNow() {
// Navigation vers l'inscription
}
void _onStartRegistration() {
// Démarrer le processus d'inscription
}
}
/// Painter pour le motif d'accueil
class _WelcomePatternPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.1)
..strokeWidth = 1
..style = PaintingStyle.stroke;
// Dessiner des cercles concentriques
for (int i = 1; i <= 5; i++) {
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
i * size.width / 10,
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -0,0 +1,132 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import '../../domain/entities/dashboard_entity.dart';
/// Générateur de données pour les graphiques basé sur les stats réelles
class ChartDataGenerator {
/// Génère des FlSpots pour un graphique de croissance sur 12 mois
/// basé sur la valeur actuelle et le taux de croissance
static List<FlSpot> generateMonthlyGrowthSpots({
required double currentValue,
required double monthlyGrowthRate,
}) {
// Si pas de données, retourner un graphique plat minimum
if (currentValue == 0) {
return List.generate(12, (index) => FlSpot(index.toDouble(), 100.0));
}
final spots = <FlSpot>[];
final random = Random(42); // Seed fixe pour cohérence
// Calculer la valeur de départ (il y a 11 mois)
final startValue = currentValue / pow(1 + monthlyGrowthRate, 11);
for (int i = 0; i < 12; i++) {
// Calculer la valeur avec croissance + variation aléatoire
final baseValue = startValue * pow(1 + monthlyGrowthRate, i);
final variance = baseValue * 0.05 * (random.nextDouble() - 0.5); // ±2.5% variance
final value = baseValue + variance;
spots.add(FlSpot(i.toDouble(), value.clamp(0, double.infinity)));
}
return spots;
}
/// Génère des FlSpots basés sur DashboardStatsEntity
static List<FlSpot> generateGrowthSpotsFromStats(DashboardStatsEntity? stats) {
if (stats == null) {
return generateMonthlyGrowthSpots(currentValue: 0, monthlyGrowthRate: 0);
}
// Utiliser totalContributionAmount comme valeur de référence
final currentValue = stats.totalContributionAmount;
// Utiliser monthlyGrowth (déjà en pourcentage) converti en taux
final monthlyGrowthRate = stats.monthlyGrowth / 100;
return generateMonthlyGrowthSpots(
currentValue: currentValue,
monthlyGrowthRate: monthlyGrowthRate.clamp(-0.5, 0.5), // Limiter à ±50% par mois
);
}
/// Génère des FlSpots pour un graphique de membres actifs vs inactifs
static List<FlSpot> generateMemberActivitySpots(DashboardStatsEntity? stats) {
if (stats == null || stats.totalMembers == 0) {
return List.generate(12, (index) => FlSpot(index.toDouble(), 50.0));
}
final activePercentage = (stats.activeMembers / stats.totalMembers) * 100;
final random = Random(43);
return List.generate(12, (index) {
// Tendance graduelle vers le taux actuel
final targetValue = activePercentage;
final startValue = max(20.0, targetValue - 20); // Commencer 20% plus bas
final progress = index / 11;
final baseValue = startValue + (targetValue - startValue) * progress;
final variance = 5 * (random.nextDouble() - 0.5); // ±2.5% variance
return FlSpot(index.toDouble(), (baseValue + variance).clamp(0, 100));
});
}
/// Génère des FlSpots pour un graphique d'engagement sur 12 mois
static List<FlSpot> generateEngagementSpots(DashboardStatsEntity? stats) {
if (stats == null) {
return List.generate(12, (index) => FlSpot(index.toDouble(), 50.0));
}
final currentEngagement = stats.engagementRate * 100;
final random = Random(44);
return List.generate(12, (index) {
final targetValue = currentEngagement;
final startValue = max(30.0, targetValue - 15);
final progress = index / 11;
final baseValue = startValue + (targetValue - startValue) * progress;
final variance = 8 * (random.nextDouble() - 0.5);
return FlSpot(index.toDouble(), (baseValue + variance).clamp(0, 100));
});
}
/// Génère des FlSpots pour un graphique d'événements sur 12 mois
static List<FlSpot> generateEventsSpots(DashboardStatsEntity? stats) {
if (stats == null || stats.totalEvents == 0) {
return List.generate(12, (index) => FlSpot(index.toDouble(), 2.0));
}
final avgEventsPerMonth = stats.totalEvents / 12;
final random = Random(45);
return List.generate(12, (index) {
final baseValue = avgEventsPerMonth;
final variance = baseValue * 0.4 * (random.nextDouble() - 0.5);
final value = baseValue + variance;
return FlSpot(index.toDouble(), value.clamp(0, double.infinity));
});
}
/// Génère des FlSpots pour les contributions sur 12 mois
static List<FlSpot> generateContributionSpots(DashboardStatsEntity? stats) {
if (stats == null || stats.totalContributionAmount == 0) {
return List.generate(12, (index) => FlSpot(index.toDouble(), 1000.0));
}
final avgPerMonth = stats.totalContributionAmount / 12;
final random = Random(46);
return List.generate(12, (index) {
// Tendance croissante vers la fin
final seasonality = 1 + (index / 11) * 0.3; // +30% croissance sur l'année
final baseValue = avgPerMonth * seasonality;
final variance = baseValue * 0.25 * (random.nextDouble() - 0.5);
final value = baseValue + variance;
return FlSpot(index.toDouble(), value.clamp(0, double.infinity));
});
}
}

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
/// Widget de graphique pour le dashboard
class DashboardChartWidget extends StatelessWidget {
@@ -20,14 +21,13 @@ class DashboardChartWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
SizedBox(
height: height,
child: BlocBuilder<DashboardBloc, DashboardState>(
@@ -54,23 +54,16 @@ class DashboardChartWidget extends StatelessWidget {
Widget _buildHeader() {
return Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Icon(
_getChartIcon(),
color: DashboardTheme.royalBlue,
size: 20,
),
Icon(
_getChartIcon(),
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: DashboardTheme.titleMedium,
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
],
@@ -97,22 +90,22 @@ class DashboardChartWidget extends StatelessWidget {
centerSpaceRadius: 40,
sections: [
PieChartSectionData(
color: DashboardTheme.success,
color: AppColors.success,
value: stats.activeMembers.toDouble(),
title: '${stats.activeMembers}',
radius: 50,
titleStyle: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
titleStyle: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
PieChartSectionData(
color: DashboardTheme.grey300,
color: AppColors.lightBorder,
value: (stats.totalMembers - stats.activeMembers).toDouble(),
title: '${stats.totalMembers - stats.activeMembers}',
radius: 45,
titleStyle: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey700,
titleStyle: AppTypography.badgeText.copyWith(
color: AppColors.textSecondaryLight,
fontWeight: FontWeight.bold,
),
),
@@ -130,7 +123,7 @@ class DashboardChartWidget extends StatelessWidget {
horizontalInterval: stats.totalContributionAmount / 4,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: DashboardTheme.grey200,
color: AppColors.lightBorder,
strokeWidth: 1,
);
},
@@ -149,7 +142,7 @@ class DashboardChartWidget extends StatelessWidget {
if (value.toInt() >= 0 && value.toInt() < months.length) {
return Text(
months[value.toInt()],
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 8),
);
}
return const Text('');
@@ -164,7 +157,7 @@ class DashboardChartWidget extends StatelessWidget {
getTitlesWidget: (double value, TitleMeta meta) {
return Text(
'${(value / 1000).toStringAsFixed(0)}K',
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 8),
);
},
),
@@ -181,8 +174,8 @@ class DashboardChartWidget extends StatelessWidget {
isCurved: true,
gradient: const LinearGradient(
colors: [
DashboardTheme.tealBlue,
DashboardTheme.royalBlue,
AppColors.brandGreen,
AppColors.primaryGreen,
],
),
barWidth: 3,
@@ -192,8 +185,8 @@ class DashboardChartWidget extends StatelessWidget {
show: true,
gradient: LinearGradient(
colors: [
DashboardTheme.tealBlue.withOpacity(0.3),
DashboardTheme.royalBlue.withOpacity(0.1),
AppColors.brandGreen.withOpacity(0.3),
AppColors.primaryGreen.withOpacity(0.1),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
@@ -228,7 +221,7 @@ class DashboardChartWidget extends StatelessWidget {
events[value.toInt()].title.length > 8
? '${events[value.toInt()].title.substring(0, 8)}...'
: events[value.toInt()].title,
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 8),
textAlign: TextAlign.center,
),
);
@@ -252,10 +245,10 @@ class DashboardChartWidget extends StatelessWidget {
BarChartRodData(
toY: event.currentParticipants.toDouble(),
color: event.isFull
? DashboardTheme.error
? AppColors.error
: event.isAlmostFull
? DashboardTheme.warning
: DashboardTheme.success,
? AppColors.warning
: AppColors.success,
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
@@ -283,13 +276,13 @@ class DashboardChartWidget extends StatelessWidget {
LineChartBarData(
spots: _generateGrowthSpots(stats.monthlyGrowth),
isCurved: true,
color: stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
color: stats.hasGrowth ? AppColors.success : AppColors.error,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: (stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error)
color: (stats.hasGrowth ? AppColors.success : AppColors.error)
.withOpacity(0.2),
),
),
@@ -321,12 +314,13 @@ class DashboardChartWidget extends StatelessWidget {
Widget _buildLoadingChart() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.grey100,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: AppColors.lightBorder.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: CircularProgressIndicator(
color: DashboardTheme.royalBlue,
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
),
),
);
@@ -335,8 +329,8 @@ class DashboardChartWidget extends StatelessWidget {
Widget _buildErrorChart() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
@@ -344,14 +338,16 @@ class DashboardChartWidget extends StatelessWidget {
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 32,
color: AppColors.error,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
'ERREUR',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
@@ -363,23 +359,24 @@ class DashboardChartWidget extends StatelessWidget {
Widget _buildEmptyChart() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.grey50,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: AppColors.lightBorder.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.bar_chart,
color: DashboardTheme.grey400,
size: 32,
Icons.bar_chart_outlined,
color: AppColors.textSecondaryLight,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Aucune donnée',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
'AUCUNE DONNÉE',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
@@ -391,13 +388,13 @@ class DashboardChartWidget extends StatelessWidget {
IconData _getChartIcon() {
switch (chartType) {
case DashboardChartType.memberActivity:
return Icons.pie_chart;
return Icons.pie_chart_outline;
case DashboardChartType.contributionTrend:
return Icons.trending_up;
return Icons.trending_up_outlined;
case DashboardChartType.eventParticipation:
return Icons.bar_chart;
return Icons.bar_chart_outlined;
case DashboardChartType.monthlyGrowth:
return Icons.show_chart;
return Icons.show_chart_outlined;
}
}
}

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../../../shared/widgets/mini_avatar.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
/// Widget des activités récentes connecté au backend
class ConnectedRecentActivities extends StatelessWidget {
@@ -21,14 +23,13 @@ class ConnectedRecentActivities extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
@@ -37,7 +38,7 @@ class ConnectedRecentActivities extends StatelessWidget {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildActivitiesList(data.recentActivities);
return _buildActivitiesList(context, data.recentActivities);
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
@@ -52,33 +53,26 @@ class ConnectedRecentActivities extends StatelessWidget {
Widget _buildHeader() {
return Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.tealBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.history,
color: DashboardTheme.tealBlue,
size: 20,
),
const Icon(
Icons.history,
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: DashboardTheme.spacing12),
const Expanded(
const SizedBox(width: 8),
Expanded(
child: Text(
'Activités récentes',
style: DashboardTheme.titleMedium,
'ACTIVITÉS RÉCENTES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
if (onSeeAll != null)
TextButton(
onPressed: onSeeAll,
GestureDetector(
onTap: onSeeAll,
child: Text(
'Voir tout',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
'TOUT VOIR',
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
),
@@ -86,7 +80,7 @@ class ConnectedRecentActivities extends StatelessWidget {
);
}
Widget _buildActivitiesList(List<RecentActivityEntity> activities) {
Widget _buildActivitiesList(BuildContext context, List<RecentActivityEntity> activities) {
if (activities.isEmpty) {
return _buildEmptyState();
}
@@ -101,79 +95,60 @@ class ConnectedRecentActivities extends StatelessWidget {
return Column(
children: [
_buildActivityItem(activity),
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
_buildActivityItem(context, activity),
if (!isLast) const SizedBox(height: 12),
],
);
}).toList(),
);
}
Widget _buildActivityItem(RecentActivityEntity activity) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar ou icône
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _getActivityColor(activity.type).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
Widget _buildActivityItem(BuildContext context, RecentActivityEntity activity) {
return InkWell(
onTap: () => _navigateForActivity(context, activity),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MiniAvatar(
fallbackText: activity.userName.isNotEmpty ? activity.userName[0].toUpperCase() : '?',
imageUrl: activity.userAvatar,
size: 32,
),
child: activity.userAvatar != null
? ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
activity.userAvatar!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Icon(
_getActivityIcon(activity.type),
color: _getActivityColor(activity.type),
size: 20,
),
),
)
: Icon(
_getActivityIcon(activity.type),
color: _getActivityColor(activity.type),
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
// Contenu
Expanded(
const SizedBox(width: 12),
// Contenu
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2,
style: AppTypography.actionText.copyWith(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
activity.description,
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 2),
Row(
children: [
Text(
activity.userName,
style: DashboardTheme.bodySmall.copyWith(
fontWeight: FontWeight.w500,
color: DashboardTheme.royalBlue,
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primaryGreen,
fontSize: 9,
),
),
Text(
'${activity.timeAgo}',
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
),
],
),
@@ -182,15 +157,14 @@ class ConnectedRecentActivities extends StatelessWidget {
),
// Action button si disponible
if (activity.hasAction)
IconButton(
onPressed: () => _navigateForActivity(context, activity),
icon: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: DashboardTheme.grey400,
),
const Icon(
Icons.chevron_right,
size: 14,
color: AppColors.textSecondaryLight,
),
],
],
),
),
);
}
@@ -220,7 +194,7 @@ class ConnectedRecentActivities extends StatelessWidget {
children: List.generate(3, (index) => Column(
children: [
_buildLoadingItem(),
if (index < 2) const SizedBox(height: DashboardTheme.spacing12),
if (index < 2) const SizedBox(height: 12),
],
)),
);
@@ -233,11 +207,11 @@ class ConnectedRecentActivities extends StatelessWidget {
width: 40,
height: 40,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(20),
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -246,25 +220,25 @@ class ConnectedRecentActivities extends StatelessWidget {
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 4),
Container(
height: 12,
width: 200,
decoration: BoxDecoration(
color: DashboardTheme.grey100,
color: AppColors.lightBorder.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 4),
Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: DashboardTheme.grey100,
color: AppColors.lightBorder.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
@@ -279,24 +253,9 @@ class ConnectedRecentActivities extends StatelessWidget {
return Center(
child: Column(
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 48,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
message,
style: DashboardTheme.bodySmall,
textAlign: TextAlign.center,
),
const Icon(Icons.error_outline, color: AppColors.error, size: 32),
const SizedBox(height: 8),
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
],
),
);
@@ -306,24 +265,10 @@ class ConnectedRecentActivities extends StatelessWidget {
return Center(
child: Column(
children: [
const Icon(
Icons.history,
color: DashboardTheme.grey400,
size: 48,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Aucune activité récente',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
),
const SizedBox(height: DashboardTheme.spacing4),
const Text(
'Les activités apparaîtront ici',
style: DashboardTheme.bodySmall,
textAlign: TextAlign.center,
),
const Icon(Icons.history, color: AppColors.textSecondaryLight, size: 32),
const SizedBox(height: 8),
const Text('AUCUNE ACTIVITÉ', style: AppTypography.subtitleSmall),
Text('Les activités apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
],
),
);
@@ -349,17 +294,17 @@ class ConnectedRecentActivities extends StatelessWidget {
Color _getActivityColor(String type) {
switch (type.toLowerCase()) {
case 'member':
return DashboardTheme.success;
return AppColors.success;
case 'event':
return DashboardTheme.info;
return AppColors.info;
case 'contribution':
return DashboardTheme.tealBlue;
return AppColors.brandGreen;
case 'organization':
return DashboardTheme.royalBlue;
return AppColors.primaryGreen;
case 'system':
return DashboardTheme.warning;
return AppColors.warning;
default:
return DashboardTheme.grey500;
return AppColors.textSecondaryLight;
}
}
}

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../domain/entities/dashboard_entity.dart';
/// Widget de carte de statistiques connecté au backend
class ConnectedStatsCard extends StatelessWidget {
@@ -45,157 +46,85 @@ class ConnectedStatsCard extends StatelessWidget {
Widget _buildDataCard(DashboardStatsEntity stats) {
final value = valueExtractor(stats);
final subtitle = subtitleExtractor?.call(stats);
final color = customColor ?? DashboardTheme.royalBlue;
final color = customColor ?? AppColors.primaryGreen;
return GestureDetector(
return CoreCard(
onTap: onTap,
child: Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Text(
title,
style: DashboardTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
Text(
value,
style: DashboardTheme.metricLarge.copyWith(color: color),
),
if (subtitle != null) ...[
const SizedBox(height: DashboardTheme.spacing4),
Text(
subtitle,
style: DashboardTheme.bodySmall,
),
],
],
),
),
);
}
Widget _buildLoadingCard() {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
Expanded(
child: Container(
height: 16,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
child: Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
Container(
height: 32,
width: 80,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
const SizedBox(height: 12),
Text(
value,
style: AppTypography.headerSmall.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: DashboardTheme.grey100,
borderRadius: BorderRadius.circular(4),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
),
],
],
),
);
}
Widget _buildLoadingCard() {
return const CoreCard(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// On peut utiliser un Shimmer ici si disponible
CircularProgressIndicator(strokeWidth: 2),
],
),
);
}
Widget _buildErrorCard(String message) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 24,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Text(
title,
style: DashboardTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
Text(
'--',
style: DashboardTheme.metricLarge.copyWith(
color: DashboardTheme.grey400,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
message,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.error,
),
),
const Icon(Icons.error_outline, color: AppColors.error, size: 20),
const SizedBox(height: 8),
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
],
),
);

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
/// Widget des événements à venir connecté au backend
class ConnectedUpcomingEvents extends StatelessWidget {
@@ -17,23 +18,22 @@ class ConnectedUpcomingEvents extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
builder: (ctx, state) {
if (state is DashboardLoading) {
return _buildLoadingList();
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildEventsList(data.upcomingEvents);
return _buildEventsList(context, data.upcomingEvents);
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
@@ -48,33 +48,26 @@ class ConnectedUpcomingEvents extends StatelessWidget {
Widget _buildHeader() {
return Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.event,
color: DashboardTheme.royalBlue,
size: 20,
),
const Icon(
Icons.event_outlined,
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: DashboardTheme.spacing12),
const Expanded(
const SizedBox(width: 8),
Expanded(
child: Text(
'Événements à venir',
style: DashboardTheme.titleMedium,
'ÉVÉNEMENTS À VENIR',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
if (onSeeAll != null)
TextButton(
onPressed: onSeeAll,
GestureDetector(
onTap: onSeeAll,
child: Text(
'Voir tout',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
'TOUT VOIR',
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
),
@@ -82,7 +75,7 @@ class ConnectedUpcomingEvents extends StatelessWidget {
);
}
Widget _buildEventsList(List<UpcomingEventEntity> events) {
Widget _buildEventsList(BuildContext context, List<UpcomingEventEntity> events) {
if (events.isEmpty) {
return _buildEmptyState();
}
@@ -97,86 +90,62 @@ class ConnectedUpcomingEvents extends StatelessWidget {
return Column(
children: [
_buildEventCard(event),
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
_buildEventCard(context, event),
if (!isLast) const SizedBox(height: 12),
],
);
}).toList(),
);
}
Widget _buildEventCard(UpcomingEventEntity event) {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.grey50,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(
color: event.isToday
? DashboardTheme.success
: event.isTomorrow
? DashboardTheme.warning
: DashboardTheme.grey200,
width: event.isToday || event.isTomorrow ? 2 : 1,
),
),
padding: const EdgeInsets.all(DashboardTheme.spacing12),
Widget _buildEventCard(BuildContext context, UpcomingEventEntity event) {
final statusColor = event.isToday ? AppColors.success : (event.isTomorrow ? AppColors.warning : AppColors.primaryGreen);
return CoreCard(
backgroundColor: Theme.of(context).cardColor,
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Image ou icône
Container(
width: 50,
height: 50,
width: 44,
height: 44,
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: event.imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(8),
child: Image.network(
event.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.event,
color: DashboardTheme.royalBlue,
size: 24,
),
errorBuilder: (context, error, stackTrace) => Icon(Icons.event_outlined, color: statusColor, size: 20),
),
)
: const Icon(
Icons.event,
color: DashboardTheme.royalBlue,
size: 24,
),
: Icon(Icons.event_outlined, color: statusColor, size: 20),
),
const SizedBox(width: DashboardTheme.spacing12),
// Contenu principal
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: DashboardTheme.titleSmall,
maxLines: 2,
style: AppTypography.actionText.copyWith(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: DashboardTheme.spacing4),
Row(
children: [
const Icon(
Icons.location_on,
size: 14,
color: DashboardTheme.grey500,
),
const SizedBox(width: DashboardTheme.spacing4),
const Icon(Icons.location_on_outlined, size: 10, color: AppColors.textSecondaryLight),
const SizedBox(width: 4),
Expanded(
child: Text(
event.location,
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -186,99 +155,47 @@ class ConnectedUpcomingEvents extends StatelessWidget {
],
),
),
// Badge de temps
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: event.isToday
? DashboardTheme.success.withOpacity(0.1)
: event.isTomorrow
? DashboardTheme.warning.withOpacity(0.1)
: DashboardTheme.royalBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
event.daysUntilEvent,
style: DashboardTheme.bodySmall.copyWith(
color: event.isToday
? DashboardTheme.success
: event.isTomorrow
? DashboardTheme.warning
: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
),
event.daysUntilEvent.toUpperCase(),
style: AppTypography.badgeText.copyWith(color: statusColor, fontSize: 8, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: DashboardTheme.spacing12),
// Barre de progression des participants
Row(
const SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Participants',
style: DashboardTheme.bodySmall,
),
Text(
'${event.currentParticipants}/${event.maxParticipants}',
style: DashboardTheme.bodySmall.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing4),
LinearProgressIndicator(
value: event.fillPercentage,
backgroundColor: DashboardTheme.grey200,
valueColor: AlwaysStoppedAnimation<Color>(
event.isFull
? DashboardTheme.error
: event.isAlmostFull
? DashboardTheme.warning
: DashboardTheme.success,
),
),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('PARTICIPANTS', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text(
'${event.currentParticipants}/${event.maxParticipants}',
style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: event.fillPercentage,
minHeight: 4,
backgroundColor: AppColors.lightBorder,
valueColor: AlwaysStoppedAnimation<Color>(
event.isFull ? AppColors.error : (event.isAlmostFull ? AppColors.warning : AppColors.success),
),
),
),
],
),
// Tags
if (event.tags.isNotEmpty) ...[
const SizedBox(height: DashboardTheme.spacing8),
Wrap(
spacing: DashboardTheme.spacing4,
runSpacing: DashboardTheme.spacing4,
children: event.tags.take(3).map((tag) => Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
),
decoration: BoxDecoration(
color: DashboardTheme.tealBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
tag,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.tealBlue,
fontWeight: FontWeight.w500,
),
),
)).toList(),
),
],
],
),
);
@@ -289,78 +206,15 @@ class ConnectedUpcomingEvents extends StatelessWidget {
children: List.generate(2, (index) => Column(
children: [
_buildLoadingCard(),
if (index < 1) const SizedBox(height: DashboardTheme.spacing12),
if (index < 1) const SizedBox(height: 12),
],
)),
);
}
Widget _buildLoadingCard() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.grey50,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(color: DashboardTheme.grey200),
),
padding: const EdgeInsets.all(DashboardTheme.spacing12),
child: Column(
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: DashboardTheme.spacing4),
Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: DashboardTheme.grey100,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
Container(
width: 60,
height: 24,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(12),
),
),
],
),
const SizedBox(height: DashboardTheme.spacing12),
Container(
height: 4,
width: double.infinity,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(2),
),
),
],
),
return const CoreCard(
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
@@ -368,24 +222,9 @@ class ConnectedUpcomingEvents extends StatelessWidget {
return Center(
child: Column(
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 48,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
message,
style: DashboardTheme.bodySmall,
textAlign: TextAlign.center,
),
const Icon(Icons.error_outline, color: AppColors.error, size: 32),
const SizedBox(height: 8),
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
],
),
);
@@ -395,24 +234,10 @@ class ConnectedUpcomingEvents extends StatelessWidget {
return Center(
child: Column(
children: [
const Icon(
Icons.event_busy,
color: DashboardTheme.grey400,
size: 48,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Aucun événement à venir',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
),
const SizedBox(height: DashboardTheme.spacing4),
const Text(
'Les événements apparaîtront ici',
style: DashboardTheme.bodySmall,
textAlign: TextAlign.center,
),
const Icon(Icons.event_outlined, color: AppColors.textSecondaryLight, size: 32),
const SizedBox(height: 8),
const Text('AUCUN ÉVÉNEMENT', style: AppTypography.subtitleSmall),
Text('Les événements apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
],
),
);

View File

@@ -3,189 +3,233 @@
library dashboard_drawer;
import 'package:flutter/material.dart';
import '../../../../shared/design_system/tokens/color_tokens.dart';
import '../../../../shared/design_system/tokens/spacing_tokens.dart';
import '../../../../shared/design_system/tokens/typography_tokens.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Modèle de données pour un élément de menu
class DrawerMenuItem {
/// Icône de l'élément de menu
final IconData icon;
/// Titre de l'élément de menu
final String title;
/// Callback lors du tap sur l'élément
final VoidCallback? onTap;
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../../../../shared/widgets/mini_avatar.dart';
/// Constructeur du modèle d'élément de menu
const DrawerMenuItem({
required this.icon,
required this.title,
this.onTap,
});
}
import '../../../authentication/presentation/bloc/auth_bloc.dart';
/// Widget de menu latéral
///
/// Affiche la navigation principale avec :
/// - Header avec profil utilisateur
/// - Menu de navigation structuré
/// - Actions secondaires
/// - Design Material avec gradient
import '../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../../help/presentation/pages/help_support_page.dart';
import '../../../about/presentation/pages/about_page.dart';
/// Widget de menu latéral (Drawer / Hamburger)
///
/// Accessible via le bouton hamburger de l'AppBar.
/// Contient uniquement les menus « Mon Espace » :
/// - Mon Profil
/// - Notifications
/// - Aide & Support
/// - À propos
/// - Déconnexion
class DashboardDrawer extends StatelessWidget {
/// Callback pour les actions de navigation
/// Callback pour les actions de navigation nommée (optionnel, non utilisé en interne)
final Function(String route)? onNavigate;
/// Callback pour la déconnexion
final VoidCallback? onLogout;
/// Constructeur du menu latéral
const DashboardDrawer({
super.key,
this.onNavigate,
this.onLogout,
});
/// Génère la liste des éléments de menu principaux
List<DrawerMenuItem> _getMainMenuItems() {
return [
DrawerMenuItem(
icon: Icons.dashboard,
title: 'Dashboard',
onTap: () => onNavigate?.call('/dashboard'),
),
DrawerMenuItem(
icon: Icons.people,
title: 'Membres',
onTap: () => onNavigate?.call('/members'),
),
DrawerMenuItem(
icon: Icons.account_balance_wallet,
title: 'Cotisations',
onTap: () => onNavigate?.call('/cotisations'),
),
DrawerMenuItem(
icon: Icons.event,
title: 'Événements',
onTap: () => onNavigate?.call('/events'),
),
DrawerMenuItem(
icon: Icons.favorite,
title: 'Solidarité',
onTap: () => onNavigate?.call('/solidarity'),
),
];
}
/// Génère la liste des éléments de menu secondaires
List<DrawerMenuItem> _getSecondaryMenuItems() {
return [
DrawerMenuItem(
icon: Icons.analytics,
title: 'Rapports',
onTap: () => onNavigate?.call('/reports'),
),
DrawerMenuItem(
icon: Icons.settings,
title: 'Paramètres',
onTap: () => onNavigate?.call('/settings'),
),
DrawerMenuItem(
icon: Icons.help,
title: 'Aide',
onTap: () => onNavigate?.call('/help'),
),
];
}
@override
Widget build(BuildContext context) {
final mainItems = _getMainMenuItems();
final secondaryItems = _getSecondaryMenuItems();
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
if (authState is! AuthAuthenticated) {
return const Drawer();
}
final state = authState;
return Drawer(
backgroundColor: ColorTokens.background,
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── En-tête utilisateur (même style que MorePage) ──────────────
_buildUserProfile(state),
const SizedBox(height: SpacingTokens.md),
// ── Section Mon Espace ─────────────────────────────────────────
_buildSectionTitle('Mon Espace'),
_buildOptionTile(
context: context,
icon: Icons.person,
title: 'Mon Profil',
subtitle: 'Modifier mes informations',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
),
),
_buildOptionTile(
context: context,
icon: Icons.notifications,
title: 'Notifications',
subtitle: 'Gérer les notifications',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NotificationsPageWrapper()),
),
),
_buildOptionTile(
context: context,
icon: Icons.help,
title: 'Aide & Support',
subtitle: 'Documentation et support',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const HelpSupportPage()),
),
),
_buildOptionTile(
context: context,
icon: Icons.info,
title: 'À propos',
subtitle: 'Version et informations',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const AboutPage()),
),
),
const SizedBox(height: SpacingTokens.md),
// ── Déconnexion ───────────────────────────────────────────────
_buildOptionTile(
context: context,
icon: Icons.logout,
title: 'Déconnexion',
subtitle: 'Se déconnecter de l\'application',
color: ColorTokens.error,
onTap: () {
Navigator.pop(context);
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
),
],
),
),
),
);
},
);
}
// ── Profil utilisateur (idem MorePage._buildUserProfile) ──────────────────
Widget _buildUserProfile(AuthAuthenticated state) {
return CoreCard(
child: Row(
children: [
_buildDrawerHeader(),
...mainItems.map((item) => _buildMenuItem(item)),
const Divider(),
...secondaryItems.map((item) => _buildMenuItem(item)),
const Divider(),
_buildLogoutItem(),
MiniAvatar(
fallbackText:
state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U',
size: 40,
imageUrl: state.user.avatar,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${state.user.firstName} ${state.user.lastName}',
style: AppTypography.actionText,
),
Text(
state.effectiveRole.displayName.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
Text(
state.user.email,
style: AppTypography.subtitleSmall,
),
],
),
),
],
),
);
}
/// Construit l'en-tête du drawer avec profil utilisateur
Widget _buildDrawerHeader() {
return DrawerHeader(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [ColorTokens.primary, ColorTokens.secondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
// ── Titre de section (idem MorePage._buildSectionTitle) ───────────────────
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(top: 24, bottom: 8, left: 4),
child: Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: AppColors.textSecondaryLight,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
);
}
// ── Tuile d'option (idem MorePage._buildOptionTile) ───────────────────────
Widget _buildOptionTile({
required BuildContext context,
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
Color? color,
}) {
final effectiveColor = color ?? AppColors.primaryGreen;
return CoreCard(
margin: const EdgeInsets.only(bottom: 8),
onTap: onTap,
child: Row(
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.person,
size: 35,
color: ColorTokens.primary,
icon,
color: effectiveColor,
size: 20,
),
),
const SizedBox(height: SpacingTokens.md),
Text(
'Utilisateur UnionFlow',
style: TypographyTokens.titleMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.actionText.copyWith(
color: color ?? AppColors.textPrimaryLight,
),
),
Text(
subtitle,
style: AppTypography.subtitleSmall,
),
],
),
),
Text(
'admin@unionflow.dev',
style: TypographyTokens.bodySmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
Icon(
Icons.chevron_right,
color: AppColors.textSecondaryLight,
size: 16,
),
],
),
);
}
/// Construit un élément de menu
Widget _buildMenuItem(DrawerMenuItem item) {
return ListTile(
leading: Icon(item.icon),
title: Text(
item.title,
style: TypographyTokens.bodyMedium,
),
onTap: item.onTap,
);
}
/// Construit l'élément de déconnexion
Widget _buildLogoutItem() {
return ListTile(
leading: const Icon(
Icons.logout,
color: ColorTokens.error,
),
title: Text(
'Déconnexion',
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.error,
),
),
onTap: onLogout,
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
/// Widget de statistique simple pour les dashboards de rôle
class DashboardStat extends StatelessWidget {
@@ -18,13 +19,8 @@ class DashboardStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.cardShadow,
),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -32,22 +28,27 @@ class DashboardStat extends StatelessWidget {
children: [
Icon(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 24,
color: color ?? AppColors.primaryGreen,
size: 20,
),
const Spacer(),
Text(
value,
style: DashboardTheme.titleLarge.copyWith(
color: color ?? DashboardTheme.royalBlue,
style: AppTypography.headerSmall.copyWith(
color: color ?? AppColors.primaryGreen,
fontSize: 18,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
title,
style: DashboardTheme.bodyMedium,
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.1,
),
),
],
),
@@ -72,9 +73,9 @@ class DashboardStatsGrid extends StatelessWidget {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.3,
children: stats,
);
}
@@ -95,9 +96,9 @@ class DashboardQuickActionsGrid extends StatelessWidget {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.5,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.4,
children: children,
);
}
@@ -120,37 +121,34 @@ class DashboardQuickAction extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
return CoreCard(
onTap: onTap,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.cardShadow,
border: Border.all(
color: (color ?? DashboardTheme.royalBlue).withOpacity(0.2),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: (color ?? AppColors.primaryGreen).withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 32,
color: color ?? AppColors.primaryGreen,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
title,
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
],
),
textAlign: TextAlign.center,
),
],
),
);
}
@@ -167,21 +165,19 @@ class DashboardRecentActivitySection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.cardShadow,
),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Activités récentes',
style: DashboardTheme.titleMedium,
Text(
'ACTIVITÉS RÉCENTES',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
...children,
],
),
@@ -209,43 +205,45 @@ class DashboardActivity extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: DashboardTheme.spacing12),
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: (color ?? DashboardTheme.royalBlue).withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: (color ?? AppColors.primaryGreen).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 16,
color: color ?? AppColors.primaryGreen,
size: 14,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.bodyMedium.copyWith(
style: AppTypography.actionText.copyWith(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
Text(
subtitle,
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
],
),
),
Text(
time,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey500,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 9,
),
),
],

View File

@@ -1,9 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:async';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
/// Widget de métriques en temps réel avec animations
class RealTimeMetricsWidget extends StatefulWidget {
@@ -81,13 +82,27 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.gradientCardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.brandGreen, AppColors.primaryGreen],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColors.primaryGreen.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing20),
const SizedBox(height: 20),
BlocConsumer<DashboardBloc, DashboardState>(
listener: (context, state) {
if (state is DashboardLoaded) {
@@ -122,37 +137,39 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
return Transform.scale(
scale: _pulseAnimation.value,
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.speed,
color: DashboardTheme.white,
size: 24,
Icons.speed_outlined,
color: Colors.white,
size: 20,
),
),
);
},
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Métriques Temps Réel',
style: DashboardTheme.titleMedium.copyWith(
color: DashboardTheme.white,
'MÉTRIQUES TEMPS RÉEL',
style: AppTypography.actionText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 2),
Text(
'Mise à jour automatique',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
'Mise à jour automatique (5 min)',
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: 10,
),
),
],
@@ -172,7 +189,7 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
);
}
@@ -185,15 +202,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
));
},
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing4),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.refresh,
color: DashboardTheme.white,
size: 16,
Icons.refresh_outlined,
color: Colors.white,
size: 14,
),
),
);
@@ -211,27 +228,27 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
children: [
Expanded(
child: _buildMetricItem(
'Membres Actifs',
'MEMBRES ACTIFS',
(data.stats.activeMembers * _countAnimation.value).round(),
data.stats.totalMembers,
Icons.people,
DashboardTheme.success,
Icons.people_outline,
AppColors.success,
),
),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
Expanded(
child: _buildMetricItem(
'Engagement',
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
100,
Icons.favorite,
DashboardTheme.warning,
AppColors.warning,
suffix: '%',
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
Row(
children: [
Expanded(
@@ -240,17 +257,17 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
(data.stats.upcomingEvents * _countAnimation.value).round(),
data.stats.totalEvents,
Icons.event,
DashboardTheme.info,
AppColors.info,
),
),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
Expanded(
child: _buildMetricItem(
'Croissance',
(data.stats.monthlyGrowth * _countAnimation.value),
null,
Icons.trending_up,
data.stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
data.stats.hasGrowth ? AppColors.success : AppColors.error,
suffix: '%',
isDecimal: true,
),
@@ -280,12 +297,12 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
}
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: DashboardTheme.white.withOpacity(0.2),
color: Colors.white.withOpacity(0.2),
),
),
child: Column(
@@ -296,34 +313,36 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
Icon(
icon,
color: color,
size: 20,
size: 16,
),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
label.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: 8,
),
),
),
],
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
displayValue,
style: DashboardTheme.titleLarge.copyWith(
color: DashboardTheme.white,
style: AppTypography.headerSmall.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24,
fontSize: 20,
),
),
if (maxValue != null) ...[
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 2),
Text(
'sur $maxValue',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.6),
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.6),
fontSize: 8,
),
),
],
@@ -338,15 +357,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
Row(
children: [
Expanded(child: _buildLoadingMetricItem()),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
Expanded(child: _buildLoadingMetricItem()),
],
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildLoadingMetricItem()),
const SizedBox(width: DashboardTheme.spacing16),
const SizedBox(width: 16),
Expanded(child: _buildLoadingMetricItem()),
],
),
@@ -357,15 +376,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
Widget _buildLoadingMetricItem() {
return Container(
height: 100,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
);
@@ -375,8 +394,8 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
return Container(
height: 200,
decoration: BoxDecoration(
color: DashboardTheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
@@ -384,14 +403,14 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
color: AppColors.error,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.error,
),
),
],
@@ -404,8 +423,8 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
return Container(
height: 200,
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
@@ -413,14 +432,14 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
children: [
Icon(
Icons.speed,
color: DashboardTheme.white.withOpacity(0.5),
color: Colors.white.withOpacity(0.5),
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Aucune donnée',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white.withOpacity(0.7),
style: AppTypography.bodyTextSmall.copyWith(
color: Colors.white.withOpacity(0.7),
),
),
],

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../../../../../shared/design_system/dashboard_theme.dart';
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../data/services/dashboard_performance_monitor.dart';
/// Widget de monitoring des performances en temps réel
@@ -127,13 +128,9 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
return _buildLoadingWidget();
}
return Container(
margin: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.subtleShadow,
),
return CoreCard(
margin: const EdgeInsets.all(8),
padding: EdgeInsets.zero,
child: Column(
children: [
_buildHeader(),
@@ -151,27 +148,23 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
}
Widget _buildLoadingWidget() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.subtleShadow,
),
child: const Row(
return CoreCard(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(16),
child: Row(
children: [
SizedBox(
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.royalBlue),
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
),
),
SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
Text(
'Initialisation du monitoring...',
style: DashboardTheme.bodyMedium,
style: AppTypography.bodyTextSmall,
),
],
),
@@ -185,9 +178,8 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
_isExpanded = !_isExpanded;
});
},
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
child: Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
child: Row(
children: [
AnimatedBuilder(
@@ -213,18 +205,24 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
);
},
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Performances Système',
style: DashboardTheme.titleSmall,
'PERFORMANCES SYSTÈME',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: AppColors.textPrimaryLight,
),
),
),
_buildQuickMetrics(),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
color: DashboardTheme.grey600,
color: AppColors.textSecondaryLight,
size: 20,
),
],
),
@@ -241,13 +239,13 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
'${_currentMetrics!.memoryUsage.toStringAsFixed(0)}MB',
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
_buildQuickMetric(
'CPU',
'${_currentMetrics!.cpuUsage.toStringAsFixed(0)}%',
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
_buildQuickMetric(
'NET',
'${_currentMetrics!.networkLatency}ms',
@@ -264,8 +262,8 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
Text(
label,
style: const TextStyle(
fontSize: 10,
color: DashboardTheme.grey600,
fontSize: 9,
color: AppColors.textSecondaryLight,
fontWeight: FontWeight.w500,
),
),
@@ -283,7 +281,7 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
Widget _buildDetailedMetrics() {
return Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildMetricRow(
@@ -293,37 +291,37 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
Icons.memory,
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: 12),
_buildMetricRow(
'Processeur',
'${_currentMetrics!.cpuUsage.toStringAsFixed(1)}%',
_currentMetrics!.cpuUsage / 100,
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
Icons.speed,
Icons.speed_outlined,
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: 12),
_buildMetricRow(
'Réseau',
'${_currentMetrics!.networkLatency} ms',
(_currentMetrics!.networkLatency / 2000).clamp(0.0, 1.0),
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
Icons.wifi,
Icons.wifi_outlined,
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: 12),
_buildMetricRow(
'Images/sec',
'${_currentMetrics!.frameRate.toStringAsFixed(1)} fps',
_currentMetrics!.frameRate / 60,
_getMetricColor(60 - _currentMetrics!.frameRate, 10, 30), // Inversé car plus c'est haut, mieux c'est
Icons.videocam,
Icons.videocam_outlined,
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: 12),
_buildMetricRow(
'Batterie',
'${_currentMetrics!.batteryLevel.toStringAsFixed(0)}%',
_currentMetrics!.batteryLevel / 100,
_getBatteryColor(_currentMetrics!.batteryLevel),
Icons.battery_std,
Icons.battery_std_outlined,
),
],
),
@@ -340,23 +338,27 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
return Row(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: Text(
label,
style: DashboardTheme.bodySmall,
style: AppTypography.subtitleSmall.copyWith(fontSize: 11),
),
),
Expanded(
flex: 3,
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: DashboardTheme.grey200,
valueColor: AlwaysStoppedAnimation<Color>(color),
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: AppColors.lightBorder,
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 4,
),
),
),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
SizedBox(
width: 60,
child: Text(
@@ -375,15 +377,15 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
Widget _buildAlertsSection() {
return Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Alertes Récentes',
style: DashboardTheme.titleSmall,
Text(
'ALERTES RÉCENTES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, fontSize: 10),
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
..._recentAlerts.take(3).map((alert) => _buildAlertItem(alert)),
],
),
@@ -402,13 +404,13 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
size: 16,
color: color,
),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
Expanded(
child: Text(
alert.message,
style: const TextStyle(
fontSize: 12,
color: DashboardTheme.grey700,
fontSize: 11,
color: AppColors.textPrimaryLight,
),
),
),
@@ -416,7 +418,7 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
_formatTime(alert.timestamp),
style: const TextStyle(
fontSize: 10,
color: DashboardTheme.grey500,
color: AppColors.textSecondaryLight,
),
),
],
@@ -425,7 +427,7 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
}
Color _getOverallHealthColor() {
if (_currentMetrics == null) return DashboardTheme.grey400;
if (_currentMetrics == null) return AppColors.textSecondaryLight;
final metrics = _currentMetrics!;
@@ -438,36 +440,36 @@ class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
switch (issues) {
case 0:
return DashboardTheme.success;
return AppColors.success;
case 1:
return DashboardTheme.warning;
return AppColors.warning;
default:
return DashboardTheme.error;
return AppColors.error;
}
}
Color _getMetricColor(double value, double warningThreshold, double errorThreshold) {
if (value >= errorThreshold) return DashboardTheme.error;
if (value >= warningThreshold) return DashboardTheme.warning;
return DashboardTheme.success;
if (value >= errorThreshold) return AppColors.error;
if (value >= warningThreshold) return AppColors.warning;
return AppColors.success;
}
Color _getBatteryColor(double batteryLevel) {
if (batteryLevel <= 20) return DashboardTheme.error;
if (batteryLevel <= 50) return DashboardTheme.warning;
return DashboardTheme.success;
if (batteryLevel <= 20) return AppColors.error;
if (batteryLevel <= 50) return AppColors.warning;
return AppColors.success;
}
Color _getAlertColor(AlertSeverity severity) {
switch (severity) {
case AlertSeverity.info:
return DashboardTheme.info;
return AppColors.info;
case AlertSeverity.warning:
return DashboardTheme.warning;
return AppColors.warning;
case AlertSeverity.error:
return DashboardTheme.error;
return AppColors.error;
case AlertSeverity.critical:
return DashboardTheme.error;
return AppColors.error;
}
}

View File

@@ -1,7 +1,13 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.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 {
@@ -80,11 +86,11 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
Widget _buildBottomNavigationBar() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.white,
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
color: DashboardTheme.grey900.withOpacity(0.1),
blurRadius: 8,
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
@@ -92,10 +98,10 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
child: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 8,
color: DashboardTheme.white,
color: Theme.of(context).cardColor,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: DashboardTheme.spacing8),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _tabs.asMap().entries.map((entry) {
@@ -121,23 +127,24 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
onTap: () => setState(() => _currentIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: DashboardTheme.spacing12,
horizontal: DashboardTheme.spacing16,
vertical: 8,
horizontal: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isActive ? tab.activeIcon : tab.icon,
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
size: 24,
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
size: 20,
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 4),
Text(
tab.title,
style: DashboardTheme.bodySmall.copyWith(
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
style: AppTypography.badgeText.copyWith(
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
fontSize: 9,
),
),
],
@@ -147,21 +154,14 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
}
Widget _buildFloatingActionButton() {
return Container(
decoration: BoxDecoration(
gradient: DashboardTheme.primaryGradient,
borderRadius: BorderRadius.circular(28),
boxShadow: DashboardTheme.elevatedShadow,
),
child: FloatingActionButton(
onPressed: _showQuickActions,
backgroundColor: Colors.transparent,
elevation: 0,
child: const Icon(
Icons.add,
color: DashboardTheme.white,
size: 28,
),
return FloatingActionButton(
onPressed: _showQuickActions,
backgroundColor: AppColors.primaryGreen,
elevation: 4,
child: const Icon(
Icons.add_outlined,
color: Colors.white,
size: 28,
),
);
}
@@ -169,9 +169,9 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
Widget _buildReportsPage() {
return Scaffold(
appBar: AppBar(
title: const Text('Rapports'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
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(
@@ -179,20 +179,20 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.assessment,
size: 64,
color: DashboardTheme.grey400,
Icons.assessment_outlined,
size: 48,
color: AppColors.textSecondaryLight,
),
const SizedBox(height: DashboardTheme.spacing16),
const Text(
'Page Rapports',
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 16),
Text(
'Fonctionnalité en cours de développement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
'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,
),
),
],
@@ -204,64 +204,64 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
Widget _buildSettingsPage() {
return Scaffold(
appBar: AppBar(
title: const Text('Paramètres'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
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(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
children: [
_buildSettingsSection(
'Apparence',
[
_buildSettingsTile(
'Thème',
'Bleu Roi & Pétrole',
Icons.palette,
() {},
'Design System UnionFlow',
Icons.palette_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Langue',
'Français',
Icons.language,
() {},
Icons.language_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const LanguageSettingsPage())),
),
],
),
const SizedBox(height: DashboardTheme.spacing24),
const SizedBox(height: 24),
_buildSettingsSection(
'Notifications',
[
_buildSettingsTile(
'Notifications push',
'Activées',
Icons.notifications,
() {},
Icons.notifications_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Emails',
'Quotidien',
Icons.email,
() {},
Icons.email_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
],
),
const SizedBox(height: DashboardTheme.spacing24),
const SizedBox(height: 24),
_buildSettingsSection(
'Données',
[
_buildSettingsTile(
'Synchronisation',
'Automatique',
Icons.sync,
() {},
Icons.sync_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Cache',
'Vider le cache',
Icons.storage,
() {},
Icons.storage_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
],
),
@@ -275,12 +275,16 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.titleMedium,
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: AppColors.primaryGreen, fontSize: 10),
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: 12),
Container(
decoration: DashboardTheme.cardDecoration,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder),
),
child: Column(children: children),
),
],
@@ -294,12 +298,13 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
VoidCallback onTap,
) {
return ListTile(
leading: Icon(icon, color: DashboardTheme.royalBlue),
title: Text(title, style: DashboardTheme.bodyMedium),
subtitle: Text(subtitle, style: DashboardTheme.bodySmall),
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,
color: DashboardTheme.grey400,
Icons.chevron_right_outlined,
color: AppColors.textSecondaryLight,
size: 16,
),
onTap: onTap,
);
@@ -310,14 +315,14 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: const BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(DashboardTheme.borderRadiusLarge),
topRight: Radius.circular(DashboardTheme.borderRadiusLarge),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
padding: const EdgeInsets.all(DashboardTheme.spacing20),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -325,61 +330,60 @@ class _DashboardNavigationState extends State<DashboardNavigation> {
width: 40,
height: 4,
decoration: BoxDecoration(
color: DashboardTheme.grey300,
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: DashboardTheme.spacing20),
const Text(
'Actions Rapides',
style: DashboardTheme.titleMedium,
const SizedBox(height: 20),
Text(
'ACTIONS RAPIDES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
const SizedBox(height: DashboardTheme.spacing20),
const SizedBox(height: 20),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: DashboardTheme.spacing16,
mainAxisSpacing: DashboardTheme.spacing16,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: [
_buildQuickActionItem('Nouveau\nMembre', Icons.person_add, DashboardTheme.success),
_buildQuickActionItem('Créer\nÉvénement', Icons.event_available, DashboardTheme.royalBlue),
_buildQuickActionItem('Ajouter\nContribution', Icons.payment, DashboardTheme.tealBlue),
_buildQuickActionItem('Envoyer\nMessage', Icons.message, DashboardTheme.warning),
_buildQuickActionItem('Générer\nRapport', Icons.assessment, DashboardTheme.info),
_buildQuickActionItem('Paramètres', Icons.settings, DashboardTheme.grey600),
_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: DashboardTheme.spacing20),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildQuickActionItem(String title, IconData icon, Color color) {
Widget _buildQuickActionItem(BuildContext context, String title, IconData icon, Color color, VoidCallback onNavigate) {
return GestureDetector(
onTap: () {
Navigator.pop(context);
// Action rapide non encore connectée
onNavigate();
},
child: Container(
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(color: color.withOpacity(0.3)),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(DashboardTheme.spacing12),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: DashboardTheme.spacing8),
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
title,
style: DashboardTheme.bodySmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textPrimaryLight,
fontSize: 9,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),

View File

@@ -2,7 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
/// Widget de notifications pour le dashboard
class DashboardNotificationsWidget extends StatelessWidget {
@@ -15,8 +19,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
return CoreCard(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -29,7 +33,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildNotifications(data);
return _buildNotifications(context, data);
} else if (state is DashboardError) {
return _buildErrorNotifications();
}
@@ -43,35 +47,36 @@ class DashboardNotificationsWidget extends StatelessWidget {
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
color: AppColors.primaryGreen.withOpacity(0.05),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(DashboardTheme.borderRadius),
topRight: Radius.circular(DashboardTheme.borderRadius),
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: AppColors.primaryGreen,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.notifications,
color: DashboardTheme.white,
size: 20,
Icons.notifications_outlined,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 10),
Expanded(
child: Text(
'Notifications',
style: DashboardTheme.titleMedium.copyWith(
color: DashboardTheme.royalBlue,
'NOTIFICATIONS',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
),
@@ -81,23 +86,24 @@ class DashboardNotificationsWidget extends StatelessWidget {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
final urgentCount = _getUrgentNotificationsCount(data);
final urgentCount = _getUrgentNotificationsCount(context, data);
if (urgentCount > 0) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: AppColors.error,
borderRadius: BorderRadius.circular(4),
),
child: Text(
urgentCount.toString(),
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 8,
),
),
);
@@ -111,8 +117,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
);
}
Widget _buildNotifications(DashboardEntity data) {
final notifications = _generateNotifications(data);
Widget _buildNotifications(BuildContext context, DashboardEntity data) {
final notifications = _generateNotifications(context, data);
if (notifications.isEmpty) {
return _buildEmptyNotifications();
@@ -127,11 +133,11 @@ class DashboardNotificationsWidget extends StatelessWidget {
Widget _buildNotificationItem(DashboardNotification notification) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: const BoxDecoration(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
color: AppColors.lightBorder,
width: 1,
),
),
@@ -140,18 +146,18 @@ class DashboardNotificationsWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: notification.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
notification.icon,
color: notification.color,
size: 20,
size: 16,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -161,7 +167,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
Expanded(
child: Text(
notification.title,
style: DashboardTheme.bodyMedium.copyWith(
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
@@ -169,40 +176,41 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (notification.isUrgent) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing6,
vertical: DashboardTheme.spacing2,
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: AppColors.error,
borderRadius: BorderRadius.circular(2),
),
child: Text(
'URGENT',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
fontSize: 7,
),
),
),
],
],
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 2),
Text(
notification.message,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey600,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 10,
),
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Row(
children: [
Text(
notification.timeAgo,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey500,
fontSize: 11,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 9,
),
),
const Spacer(),
@@ -211,9 +219,10 @@ class DashboardNotificationsWidget extends StatelessWidget {
onTap: notification.onAction,
child: Text(
notification.actionLabel!,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
fontSize: 9,
),
),
),
@@ -229,76 +238,28 @@ class DashboardNotificationsWidget extends StatelessWidget {
}
Widget _buildLoadingNotifications() {
return Column(
children: List.generate(3, (index) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
width: 1,
),
),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: DashboardTheme.spacing8),
Container(
height: 12,
width: 200,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
);
}),
return const Padding(
padding: EdgeInsets.all(20),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
Widget _buildErrorNotifications() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing24),
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 32,
color: AppColors.error,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
'Erreur',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.error,
),
),
],
@@ -309,28 +270,23 @@ class DashboardNotificationsWidget extends StatelessWidget {
Widget _buildEmptyNotifications() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing24),
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icons.notifications_none,
color: DashboardTheme.grey400,
size: 32,
Icons.notifications_none_outlined,
color: AppColors.textSecondaryLight,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Aucune notification',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
'AUCUNE NOTIFICATION',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
'Vous êtes à jour !',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey400,
),
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
],
),
@@ -338,20 +294,20 @@ class DashboardNotificationsWidget extends StatelessWidget {
);
}
List<DashboardNotification> _generateNotifications(DashboardEntity data) {
List<DashboardNotification> _generateNotifications(BuildContext context, DashboardEntity data) {
List<DashboardNotification> notifications = [];
// Notification pour les demandes en attente
if (data.stats.pendingRequests > 0) {
notifications.add(DashboardNotification(
title: 'Demandes en attente',
message: '${data.stats.pendingRequests} demandes nécessitent votre attention',
icon: Icons.pending_actions,
color: DashboardTheme.warning,
message: '${data.stats.pendingRequests} demandes à valider',
icon: Icons.pending_actions_outlined,
color: AppColors.warning,
timeAgo: '2h',
isUrgent: data.stats.pendingRequests > 20,
actionLabel: 'Voir',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
));
}
@@ -359,13 +315,13 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (data.todayEventsCount > 0) {
notifications.add(DashboardNotification(
title: 'Événements aujourd\'hui',
message: '${data.todayEventsCount} événement(s) programmé(s) aujourd\'hui',
icon: Icons.event_available,
color: DashboardTheme.info,
message: '${data.todayEventsCount} événement(s) aujourd\'hui',
icon: Icons.event_available_outlined,
color: AppColors.info,
timeAgo: '30min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
));
}
@@ -373,9 +329,9 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (data.stats.hasGrowth) {
notifications.add(DashboardNotification(
title: 'Croissance positive',
message: 'Croissance de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
icon: Icons.trending_up,
color: DashboardTheme.success,
message: 'Progression de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
icon: Icons.trending_up_outlined,
color: AppColors.success,
timeAgo: '1j',
isUrgent: false,
actionLabel: null,
@@ -386,14 +342,14 @@ class DashboardNotificationsWidget extends StatelessWidget {
// Notification pour l'engagement faible
if (!data.stats.isHighEngagement) {
notifications.add(DashboardNotification(
title: 'Engagement à améliorer',
message: 'Taux d\'engagement: ${(data.stats.engagementRate * 100).toStringAsFixed(0)}%',
icon: Icons.trending_down,
color: DashboardTheme.error,
title: 'Engagement à surveiller',
message: 'Taux: ${(data.stats.engagementRate * 100).toStringAsFixed(0)}%',
icon: Icons.trending_down_outlined,
color: AppColors.error,
timeAgo: '3h',
isUrgent: data.stats.engagementRate < 0.5,
actionLabel: 'Améliorer',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
));
}
@@ -401,21 +357,21 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (data.recentActivitiesCount > 0) {
notifications.add(DashboardNotification(
title: 'Nouvelles activités',
message: '${data.recentActivitiesCount} nouvelles activités aujourd\'hui',
icon: Icons.fiber_new,
color: DashboardTheme.tealBlue,
message: '${data.recentActivitiesCount} activités récentes',
icon: Icons.fiber_new_outlined,
color: AppColors.brandGreen,
timeAgo: '15min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
));
}
return notifications;
}
int _getUrgentNotificationsCount(DashboardEntity data) {
final notifications = _generateNotifications(data);
int _getUrgentNotificationsCount(BuildContext context, DashboardEntity data) {
final notifications = _generateNotifications(context, data);
return notifications.where((n) => n.isUrgent).length;
}
}

View File

@@ -1,5 +1,10 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/unionflow_design_system.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';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
/// Widget de recherche rapide pour le dashboard
class DashboardSearchWidget extends StatefulWidget {
@@ -26,13 +31,14 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
late Animation<double> _scaleAnimation;
bool _isExpanded = false;
List<SearchSuggestion> _filteredSuggestions = [];
List<SearchSuggestion>? _defaultSuggestions;
@override
void initState() {
super.initState();
_setupAnimations();
_setupListeners();
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
_filteredSuggestions = widget.suggestions ?? [];
}
void _setupAnimations() {
@@ -71,12 +77,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
void _filterSuggestions(String query) {
if (query.isEmpty) {
setState(() {
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
_filteredSuggestions = widget.suggestions ?? _defaultSuggestions ?? [];
});
return;
}
final filtered = (widget.suggestions ?? _getDefaultSuggestions())
final defaultList = widget.suggestions ?? _defaultSuggestions ?? [];
final filtered = defaultList
.where((suggestion) =>
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
@@ -89,11 +96,19 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
@override
Widget build(BuildContext context) {
if (_defaultSuggestions == null) {
_defaultSuggestions = _getDefaultSuggestions(context);
if (_filteredSuggestions.isEmpty && widget.suggestions == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() => _filteredSuggestions = _defaultSuggestions!);
});
}
}
return Column(
children: [
_buildSearchBar(),
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
_buildSuggestions(),
],
],
@@ -108,9 +123,11 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
scale: _scaleAnimation.value,
child: Container(
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
boxShadow: _isExpanded ? DashboardTheme.elevatedShadow : DashboardTheme.subtleShadow,
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: _isExpanded
? [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4))]
: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2))],
),
child: TextField(
controller: _searchController,
@@ -123,12 +140,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
},
decoration: InputDecoration(
hintText: widget.hintText ?? 'Rechercher...',
hintStyle: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey400,
hintStyle: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
),
prefixIcon: Icon(
Icons.search,
color: _isExpanded ? DashboardTheme.royalBlue : DashboardTheme.grey400,
Icons.search_outlined,
color: _isExpanded ? AppColors.primaryGreen : AppColors.textSecondaryLight,
size: 20,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
@@ -137,30 +155,31 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
_focusNode.unfocus();
},
icon: const Icon(
Icons.clear,
color: DashboardTheme.grey400,
Icons.close_outlined,
color: AppColors.textSecondaryLight,
size: 18,
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: DashboardTheme.royalBlue,
width: 2,
color: AppColors.primaryGreen,
width: 1.5,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing16,
vertical: DashboardTheme.spacing12,
horizontal: 16,
vertical: 12,
),
filled: true,
fillColor: DashboardTheme.white,
fillColor: Theme.of(context).cardColor,
),
style: DashboardTheme.bodyMedium,
style: AppTypography.bodyTextSmall,
),
),
);
@@ -172,9 +191,15 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
return Container(
constraints: const BoxConstraints(maxHeight: 300),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.elevatedShadow,
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListView.builder(
shrinkWrap: true,
@@ -196,13 +221,13 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
suggestion.onTap?.call();
},
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: isLast
? null
: const Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
color: AppColors.lightBorder,
width: 1,
),
),
@@ -210,34 +235,36 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: suggestion.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
suggestion.icon,
color: suggestion.color,
size: 20,
size: 18,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
suggestion.title,
style: DashboardTheme.bodyMedium.copyWith(
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
if (suggestion.subtitle.isNotEmpty) ...[
const SizedBox(height: DashboardTheme.spacing2),
const SizedBox(height: 2),
Text(
suggestion.subtitle,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey600,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 10,
),
),
],
@@ -245,8 +272,8 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
),
),
const Icon(
Icons.arrow_forward_ios,
color: DashboardTheme.grey400,
Icons.chevron_right_outlined,
color: AppColors.textSecondaryLight,
size: 16,
),
],
@@ -255,42 +282,42 @@ class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
);
}
List<SearchSuggestion> _getDefaultSuggestions() {
List<SearchSuggestion> _getDefaultSuggestions(BuildContext context) {
return [
SearchSuggestion(
title: 'Membres',
subtitle: 'Rechercher des membres',
icon: Icons.people,
color: DashboardTheme.royalBlue,
onTap: () {},
icon: Icons.people_outline,
color: AppColors.primaryGreen,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
),
SearchSuggestion(
title: 'Événements',
subtitle: 'Trouver des événements',
icon: Icons.event,
color: DashboardTheme.tealBlue,
onTap: () {},
icon: Icons.event_outlined,
color: AppColors.brandGreen,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
),
SearchSuggestion(
title: 'Contributions',
subtitle: 'Historique des paiements',
icon: Icons.payment,
color: DashboardTheme.success,
onTap: () {},
icon: Icons.account_balance_wallet_outlined,
color: AppColors.success,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
),
SearchSuggestion(
title: 'Rapports',
subtitle: 'Consulter les rapports',
icon: Icons.assessment,
color: DashboardTheme.warning,
onTap: () {},
icon: Icons.assessment_outlined,
color: AppColors.warning,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper())),
),
SearchSuggestion(
title: 'Paramètres',
subtitle: 'Configuration système',
icon: Icons.settings,
color: DashboardTheme.grey600,
onTap: () {},
icon: Icons.settings_outlined,
color: AppColors.textSecondaryLight,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
];
}

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme_manager.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../../shared/design_system/tokens/app_typography.dart';
import '../../../../../shared/design_system/tokens/spacing_tokens.dart';
import '../../../../../shared/design_system/tokens/radius_tokens.dart';
import '../../../../../shared/widgets/core_card.dart';
/// Widget de sélection de thème pour le Dashboard
class ThemeSelectorWidget extends StatefulWidget {
@@ -27,13 +31,8 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.subtleShadow,
),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -41,17 +40,17 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
children: [
Icon(
Icons.palette,
color: DashboardTheme.royalBlue,
color: AppColors.primaryGreen,
size: 24,
),
SizedBox(width: DashboardTheme.spacing8),
SizedBox(width: 8),
Text(
'Thème de l\'interface',
style: DashboardTheme.titleMedium,
style: AppTypography.headerSmall,
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: SpacingTokens.xl),
// Grille des thèmes
GridView.builder(
@@ -59,8 +58,8 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: DashboardTheme.spacing12,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.5,
),
itemCount: DashboardThemeManager.availableThemes.length,
@@ -72,7 +71,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
},
),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: SpacingTokens.xl),
// Aperçu du thème sélectionné
_buildThemePreview(),
@@ -87,11 +86,11 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? themeOption.theme.primaryColor
: DashboardTheme.grey300,
: const Color(0xFFD1D5DB),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
@@ -102,7 +101,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
offset: const Offset(0, 2),
),
]
: DashboardTheme.subtleShadow,
: null,
),
child: Column(
children: [
@@ -121,8 +120,8 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(DashboardTheme.borderRadius - 1),
topRight: Radius.circular(DashboardTheme.borderRadius - 1),
topLeft: Radius.circular(RadiusTokens.lg - 1),
topRight: Radius.circular(RadiusTokens.lg - 1),
),
),
child: isSelected
@@ -140,12 +139,12 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
flex: 1,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: themeOption.theme.cardColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(DashboardTheme.borderRadius - 1),
bottomRight: Radius.circular(DashboardTheme.borderRadius - 1),
bottomLeft: Radius.circular(7),
bottomRight: Radius.circular(7),
),
),
child: Center(
@@ -172,11 +171,11 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
.firstWhere((theme) => theme.key == _selectedTheme);
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: currentTheme.theme.backgroundColor,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(color: DashboardTheme.grey300),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFD1D5DB)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -189,15 +188,15 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
color: currentTheme.theme.textPrimary,
),
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: SpacingTokens.lg),
// Exemple de carte avec le thème
// Aperçu de carte avec le thème
Container(
width: double.infinity,
padding: const EdgeInsets.all(DashboardTheme.spacing12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: currentTheme.theme.cardColor,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: currentTheme.theme.primaryColor.withOpacity(0.1),
@@ -221,7 +220,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: SpacingTokens.lg),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -236,7 +235,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
),
const SizedBox(height: 2),
Text(
'Exemple avec ce thème',
'Aperçu',
style: TextStyle(
fontSize: 12,
color: currentTheme.theme.textSecondary,
@@ -247,12 +246,12 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: currentTheme.theme.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Actif',
@@ -267,17 +266,17 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
),
),
const SizedBox(height: DashboardTheme.spacing12),
const SizedBox(height: SpacingTokens.lg),
// Palette de couleurs
Row(
children: [
_buildColorSwatch('Primaire', currentTheme.theme.primaryColor),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
_buildColorSwatch('Secondaire', currentTheme.theme.secondaryColor),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
_buildColorSwatch('Succès', currentTheme.theme.success),
const SizedBox(width: DashboardTheme.spacing8),
const SizedBox(width: 8),
_buildColorSwatch('Attention', currentTheme.theme.warning),
],
),
@@ -295,7 +294,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
height: 30,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
@@ -303,7 +302,7 @@ class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
label,
style: const TextStyle(
fontSize: 10,
color: DashboardTheme.grey600,
color: Color(0xFF4B5563),
),
textAlign: TextAlign.center,
),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.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';
@@ -22,14 +23,13 @@ class DashboardShortcutsWidget extends StatelessWidget {
final shortcuts = customShortcuts ?? _getDefaultShortcuts(context);
final displayShortcuts = shortcuts.take(maxShortcuts).toList();
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing20),
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
const SizedBox(height: 16),
_buildShortcutsGrid(displayShortcuts),
],
),
@@ -39,36 +39,18 @@ class DashboardShortcutsWidget extends StatelessWidget {
Widget _buildHeader() {
return Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.tealBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.flash_on,
color: DashboardTheme.tealBlue,
size: 20,
),
const Icon(
Icons.flash_on_outlined,
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 8),
Expanded(
child: Text(
'Actions Rapides',
style: DashboardTheme.titleMedium.copyWith(
'ACTIONS RAPIDES',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
),
),
),
TextButton(
onPressed: () {
// Personnalisation des raccourcis non encore implémentée
},
child: Text(
'Personnaliser',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.tealBlue,
fontWeight: FontWeight.w600,
letterSpacing: 1.1,
),
),
),
@@ -82,9 +64,9 @@ class DashboardShortcutsWidget extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: DashboardTheme.spacing12,
mainAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.0,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.9,
),
itemCount: shortcuts.length,
itemBuilder: (context, index) {
@@ -96,64 +78,34 @@ class DashboardShortcutsWidget extends StatelessWidget {
Widget _buildShortcutItem(DashboardShortcut shortcut) {
return GestureDetector(
onTap: shortcut.onTap,
child: Container(
decoration: BoxDecoration(
color: shortcut.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(
color: shortcut.color.withOpacity(0.3),
width: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: shortcut.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
shortcut.icon,
color: shortcut.color,
size: 20,
),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
decoration: BoxDecoration(
color: shortcut.color.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
),
child: Icon(
shortcut.icon,
color: shortcut.color,
size: 24,
),
const SizedBox(height: 8),
Text(
shortcut.title,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textPrimaryLight,
fontSize: 9,
fontWeight: FontWeight.w500,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
shortcut.title,
style: DashboardTheme.bodySmall.copyWith(
color: shortcut.color,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (shortcut.badge != null) ...[
const SizedBox(height: DashboardTheme.spacing4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing6,
vertical: DashboardTheme.spacing2,
),
decoration: BoxDecoration(
color: shortcut.badgeColor ?? DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Text(
shortcut.badge!,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
],
],
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
@@ -162,8 +114,8 @@ class DashboardShortcutsWidget extends StatelessWidget {
return [
DashboardShortcut(
title: 'Nouveau\nMembre',
icon: Icons.person_add,
color: DashboardTheme.success,
icon: Icons.person_add_outlined,
color: AppColors.success,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
@@ -174,8 +126,8 @@ class DashboardShortcutsWidget extends StatelessWidget {
),
DashboardShortcut(
title: 'Créer\nÉvénement',
icon: Icons.event_available,
color: DashboardTheme.royalBlue,
icon: Icons.event_available_outlined,
color: AppColors.primaryGreen,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
@@ -186,32 +138,20 @@ class DashboardShortcutsWidget extends StatelessWidget {
),
DashboardShortcut(
title: 'Ajouter\nContribution',
icon: Icons.payment,
color: DashboardTheme.tealBlue,
icon: Icons.account_balance_wallet_outlined,
color: AppColors.brandGreen,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ContributionsPageWrapper(),
builder: (context) => const CotisationsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Envoyer\nMessage',
icon: Icons.message,
color: DashboardTheme.warning,
badge: '3',
badgeColor: DashboardTheme.error,
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Messagerie à venir')),
);
},
),
DashboardShortcut(
title: 'Générer\nRapport',
icon: Icons.assessment,
color: DashboardTheme.info,
icon: Icons.assessment_outlined,
color: AppColors.info,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
@@ -222,8 +162,8 @@ class DashboardShortcutsWidget extends StatelessWidget {
),
DashboardShortcut(
title: 'Paramètres',
icon: Icons.settings,
color: DashboardTheme.grey600,
icon: Icons.settings_outlined,
color: AppColors.textSecondaryLight,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(