feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles

- Config Spec-Kit pour Spec-Driven Development
- CONSTITUTION.md + .specify/memory/constitution.md
- Commandes Cursor /speckit.*, règles projet
- Mission: associations + mutuelles d'épargne et de financement
- .gitignore: versionner config spec-kit unionflow

Made-with: Cursor
This commit is contained in:
dahoud
2026-02-27 14:41:07 +00:00
parent 144b68f8e7
commit b1957c1c81
631 changed files with 104070 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
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';
part 'dashboard_event.dart';
part 'dashboard_state.dart';
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
final GetDashboardData getDashboardData;
final GetDashboardStats getDashboardStats;
final GetRecentActivities getRecentActivities;
final GetUpcomingEvents getUpcomingEvents;
DashboardBloc({
required this.getDashboardData,
required this.getDashboardStats,
required this.getRecentActivities,
required this.getUpcomingEvents,
}) : super(DashboardInitial()) {
on<LoadDashboardData>(_onLoadDashboardData);
on<RefreshDashboardData>(_onRefreshDashboardData);
on<LoadDashboardStats>(_onLoadDashboardStats);
on<LoadRecentActivities>(_onLoadRecentActivities);
on<LoadUpcomingEvents>(_onLoadUpcomingEvents);
}
Future<void> _onLoadDashboardData(
LoadDashboardData event,
Emitter<DashboardState> emit,
) async {
emit(DashboardLoading());
final result = await getDashboardData(
GetDashboardDataParams(
organizationId: event.organizationId,
userId: event.userId,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(dashboardData) => emit(DashboardLoaded(dashboardData)),
);
}
Future<void> _onRefreshDashboardData(
RefreshDashboardData event,
Emitter<DashboardState> emit,
) async {
// Garde l'état actuel pendant le refresh
if (state is DashboardLoaded) {
emit(DashboardRefreshing((state as DashboardLoaded).dashboardData));
} else {
emit(DashboardLoading());
}
final result = await getDashboardData(
GetDashboardDataParams(
organizationId: event.organizationId,
userId: event.userId,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(dashboardData) => emit(DashboardLoaded(dashboardData)),
);
}
Future<void> _onLoadDashboardStats(
LoadDashboardStats event,
Emitter<DashboardState> emit,
) async {
final result = await getDashboardStats(
GetDashboardStatsParams(
organizationId: event.organizationId,
userId: event.userId,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(stats) {
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
final updatedData = DashboardEntity(
stats: stats,
recentActivities: currentData.recentActivities,
upcomingEvents: currentData.upcomingEvents,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
}
},
);
}
Future<void> _onLoadRecentActivities(
LoadRecentActivities event,
Emitter<DashboardState> emit,
) async {
final result = await getRecentActivities(
GetRecentActivitiesParams(
organizationId: event.organizationId,
userId: event.userId,
limit: event.limit,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(activities) {
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
final updatedData = DashboardEntity(
stats: currentData.stats,
recentActivities: activities,
upcomingEvents: currentData.upcomingEvents,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
}
},
);
}
Future<void> _onLoadUpcomingEvents(
LoadUpcomingEvents event,
Emitter<DashboardState> emit,
) async {
final result = await getUpcomingEvents(
GetUpcomingEventsParams(
organizationId: event.organizationId,
userId: event.userId,
limit: event.limit,
),
);
result.fold(
(failure) => emit(DashboardError(_mapFailureToMessage(failure))),
(events) {
if (state is DashboardLoaded) {
final currentData = (state as DashboardLoaded).dashboardData;
final updatedData = DashboardEntity(
stats: currentData.stats,
recentActivities: currentData.recentActivities,
upcomingEvents: events,
userPreferences: currentData.userPreferences,
organizationId: currentData.organizationId,
userId: currentData.userId,
);
emit(DashboardLoaded(updatedData));
}
},
);
}
String _mapFailureToMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
return 'Erreur serveur. Veuillez réessayer.';
case NetworkFailure:
return 'Pas de connexion internet. Vérifiez votre connexion.';
default:
return 'Une erreur inattendue s\'est produite.';
}
}
}

View File

@@ -0,0 +1,77 @@
part of 'dashboard_bloc.dart';
abstract class DashboardEvent extends Equatable {
const DashboardEvent();
@override
List<Object> get props => [];
}
class LoadDashboardData extends DashboardEvent {
final String organizationId;
final String userId;
const LoadDashboardData({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class RefreshDashboardData extends DashboardEvent {
final String organizationId;
final String userId;
const RefreshDashboardData({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class LoadDashboardStats extends DashboardEvent {
final String organizationId;
final String userId;
const LoadDashboardStats({
required this.organizationId,
required this.userId,
});
@override
List<Object> get props => [organizationId, userId];
}
class LoadRecentActivities extends DashboardEvent {
final String organizationId;
final String userId;
final int limit;
const LoadRecentActivities({
required this.organizationId,
required this.userId,
this.limit = 10,
});
@override
List<Object> get props => [organizationId, userId, limit];
}
class LoadUpcomingEvents extends DashboardEvent {
final String organizationId;
final String userId;
final int limit;
const LoadUpcomingEvents({
required this.organizationId,
required this.userId,
this.limit = 5,
});
@override
List<Object> get props => [organizationId, userId, limit];
}

View File

@@ -0,0 +1,39 @@
part of 'dashboard_bloc.dart';
abstract class DashboardState extends Equatable {
const DashboardState();
@override
List<Object> get props => [];
}
class DashboardInitial extends DashboardState {}
class DashboardLoading extends DashboardState {}
class DashboardLoaded extends DashboardState {
final DashboardEntity dashboardData;
const DashboardLoaded(this.dashboardData);
@override
List<Object> get props => [dashboardData];
}
class DashboardRefreshing extends DashboardState {
final DashboardEntity dashboardData;
const DashboardRefreshing(this.dashboardData);
@override
List<Object> get props => [dashboardData];
}
class DashboardError extends DashboardState {
final String message;
const DashboardError(this.message);
@override
List<Object> get props => [message];
}

View File

@@ -0,0 +1,483 @@
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 '../../../../core/di/injection_container.dart';
/// Page dashboard avancée avec graphiques et analytics
class AdvancedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
const AdvancedDashboardPage({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<AdvancedDashboardPage> createState() => _AdvancedDashboardPageState();
}
class _AdvancedDashboardPageState extends State<AdvancedDashboardPage>
with TickerProviderStateMixin {
late DashboardBloc _dashboardBloc;
late TabController _tabController;
@override
void initState() {
super.initState();
_dashboardBloc = sl<DashboardBloc>();
_tabController = TabController(length: 3, vsync: this);
_loadDashboardData();
}
void _loadDashboardData() {
_dashboardBloc.add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
void _refreshDashboardData() {
_dashboardBloc.add(RefreshDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _dashboardBloc,
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
_buildSliverAppBar(),
],
body: Column(
children: [
_buildTabBar(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildOverviewTab(),
_buildAnalyticsTab(),
_buildReportsTab(),
],
),
),
],
),
),
floatingActionButton: _buildFloatingActionButton(),
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: DashboardTheme.headerDecoration,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: const Icon(
Icons.dashboard,
color: DashboardTheme.white,
size: 32,
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dashboard Avancé',
style: DashboardTheme.titleLarge.copyWith(
color: DashboardTheme.white,
fontSize: 28,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
'Analytics & Insights',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white.withOpacity(0.9),
),
),
],
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return Row(
children: [
_buildQuickStat(
'Membres',
'${data.stats.activeMembers}/${data.stats.totalMembers}',
Icons.people,
),
const SizedBox(width: DashboardTheme.spacing16),
_buildQuickStat(
'Événements',
'${data.stats.upcomingEvents}',
Icons.event,
),
const SizedBox(width: DashboardTheme.spacing16),
_buildQuickStat(
'Croissance',
'${data.stats.monthlyGrowth.toStringAsFixed(1)}%',
Icons.trending_up,
),
],
);
}
return const SizedBox.shrink();
},
),
],
),
),
),
),
),
actions: [
IconButton(
onPressed: _refreshDashboardData,
icon: const Icon(
Icons.refresh,
color: DashboardTheme.white,
),
),
IconButton(
onPressed: () {
// Navigation vers paramètres non encore connectée
},
icon: const Icon(
Icons.settings,
color: DashboardTheme.white,
),
),
],
);
}
Widget _buildQuickStat(String label, String value, IconData icon) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing12,
vertical: DashboardTheme.spacing8,
),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: DashboardTheme.white,
size: 16,
),
const SizedBox(width: DashboardTheme.spacing8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
),
),
],
),
],
),
);
}
Widget _buildTabBar() {
return Container(
color: DashboardTheme.white,
child: TabBar(
controller: _tabController,
labelColor: DashboardTheme.royalBlue,
unselectedLabelColor: DashboardTheme.grey500,
indicatorColor: DashboardTheme.royalBlue,
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)),
],
),
);
}
Widget _buildOverviewTab() {
return RefreshIndicator(
onRefresh: () async => _refreshDashboardData(),
color: DashboardTheme.royalBlue,
child: SingleChildScrollView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
// Métriques temps réel
RealTimeMetricsWidget(
organizationId: widget.organizationId,
userId: widget.userId,
),
const SizedBox(height: DashboardTheme.spacing24),
// Grille de statistiques
_buildStatsGrid(),
const SizedBox(height: DashboardTheme.spacing24),
// Notifications
const DashboardNotificationsWidget(maxNotifications: 3),
const SizedBox(height: DashboardTheme.spacing24),
// Activités et événements
const Row(
children: [
Expanded(
child: ConnectedRecentActivities(maxItems: 3),
),
SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: ConnectedUpcomingEvents(maxItems: 2),
),
],
),
],
),
),
);
}
Widget _buildAnalyticsTab() {
return const SingleChildScrollView(
padding: EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
Row(
children: [
Expanded(
child: DashboardChartWidget(
title: 'Activité des Membres',
chartType: DashboardChartType.memberActivity,
height: 250,
),
),
SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: DashboardChartWidget(
title: 'Croissance Mensuelle',
chartType: DashboardChartType.monthlyGrowth,
height: 250,
),
),
],
),
SizedBox(height: DashboardTheme.spacing24),
DashboardChartWidget(
title: 'Tendance des Contributions',
chartType: DashboardChartType.contributionTrend,
height: 300,
),
SizedBox(height: DashboardTheme.spacing24),
DashboardChartWidget(
title: 'Participation aux Événements',
chartType: DashboardChartType.eventParticipation,
height: 250,
),
],
),
);
}
Widget _buildReportsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
_buildReportCard(
'Rapport Mensuel',
'Synthèse complète des activités du mois',
Icons.calendar_month,
DashboardTheme.royalBlue,
),
const SizedBox(height: DashboardTheme.spacing16),
_buildReportCard(
'Rapport Financier',
'État des contributions et finances',
Icons.account_balance,
DashboardTheme.tealBlue,
),
const SizedBox(height: DashboardTheme.spacing16),
_buildReportCard(
'Rapport d\'Activité',
'Analyse de l\'engagement des membres',
Icons.trending_up,
DashboardTheme.success,
),
const SizedBox(height: DashboardTheme.spacing16),
_buildReportCard(
'Rapport Événements',
'Statistiques des événements organisés',
Icons.event_note,
DashboardTheme.warning,
),
],
),
);
}
Widget _buildStatsGrid() {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: DashboardTheme.spacing16,
mainAxisSpacing: DashboardTheme.spacing16,
childAspectRatio: 1.2,
children: [
ConnectedStatsCard(
title: 'Membres totaux',
icon: Icons.people,
valueExtractor: (stats) => stats.totalMembers.toString(),
subtitleExtractor: (stats) => '${stats.activeMembers} actifs',
customColor: DashboardTheme.royalBlue,
),
ConnectedStatsCard(
title: 'Contributions',
icon: Icons.payment,
valueExtractor: (stats) => stats.formattedContributionAmount,
subtitleExtractor: (stats) => '${stats.totalContributions} versements',
customColor: DashboardTheme.tealBlue,
),
ConnectedStatsCard(
title: 'Événements',
icon: Icons.event,
valueExtractor: (stats) => stats.totalEvents.toString(),
subtitleExtractor: (stats) => '${stats.upcomingEvents} à venir',
customColor: DashboardTheme.success,
),
ConnectedStatsCard(
title: 'Engagement',
icon: Icons.favorite,
valueExtractor: (stats) => '${(stats.engagementRate * 100).toStringAsFixed(0)}%',
subtitleExtractor: (stats) => stats.isHighEngagement ? 'Excellent' : 'Moyen',
customColor: DashboardTheme.warning,
),
],
);
}
Widget _buildReportCard(String title, String description, IconData icon, Color color) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.titleSmall,
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
description,
style: DashboardTheme.bodySmall,
),
],
),
),
IconButton(
onPressed: () {
// Génération de rapport non encore implémentée
},
icon: Icon(
Icons.download,
color: color,
),
),
],
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton.extended(
onPressed: () {
// Actions rapides non encore implémentées
},
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
icon: const Icon(Icons.add),
label: const Text('Action'),
);
}
@override
void dispose() {
_tabController.dispose();
_dashboardBloc.close();
super.dispose();
}
}

View File

@@ -0,0 +1,158 @@
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 '../../../../shared/design_system/dashboard_theme.dart';
/// Page dashboard connectée au backend
class ConnectedDashboardPage extends StatefulWidget {
final String organizationId;
final String userId;
const ConnectedDashboardPage({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<ConnectedDashboardPage> createState() => _ConnectedDashboardPageState();
}
class _ConnectedDashboardPageState extends State<ConnectedDashboardPage> {
@override
void initState() {
super.initState();
// Charger les données du dashboard
context.read<DashboardBloc>().add(LoadDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: DashboardTheme.grey50,
appBar: AppBar(
title: const Text('Dashboard'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
elevation: 0,
),
body: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return const Center(
child: CircularProgressIndicator(
color: DashboardTheme.royalBlue,
),
);
}
if (state is DashboardError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: DashboardTheme.error,
),
const SizedBox(height: DashboardTheme.spacing16),
const Text(
'Erreur de chargement',
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
state.message,
style: DashboardTheme.bodyMedium,
textAlign: TextAlign.center,
),
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,
),
child: const Text('Réessayer'),
),
],
),
);
}
if (state is DashboardLoaded) {
return RefreshIndicator(
onRefresh: () async {
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();
},
),
);
}
}

View File

@@ -0,0 +1,275 @@
import 'package:flutter/material.dart';
/// Dashboard simple pour Membre Actif
class ActiveMemberDashboard extends StatelessWidget {
const ActiveMemberDashboard({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête de bienvenue
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00CEC9)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bonjour !',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'Bienvenue sur votre espace membre',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
const SizedBox(height: 24),
// Statistiques rapides
const Text(
'Mes Statistiques',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildStatCard(
icon: Icons.event_available,
value: '12',
title: 'Événements',
color: const Color(0xFF00B894),
),
_buildStatCard(
icon: Icons.volunteer_activism,
value: '3',
title: 'Solidarité',
color: const Color(0xFF00CEC9),
),
_buildStatCard(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: const Color(0xFF0984E3),
),
_buildStatCard(
icon: Icons.star,
value: '4.8',
title: 'Engagement',
color: const Color(0xFFE17055),
),
],
),
const SizedBox(height: 24),
// Actions rapides
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildActionCard(
icon: Icons.event,
title: 'Créer Événement',
color: const Color(0xFF00B894),
onTap: () {},
),
_buildActionCard(
icon: Icons.volunteer_activism,
title: 'Demande Aide',
color: const Color(0xFF00CEC9),
onTap: () {},
),
_buildActionCard(
icon: Icons.account_circle,
title: 'Mon Profil',
color: const Color(0xFF0984E3),
onTap: () {},
),
_buildActionCard(
icon: Icons.message,
title: 'Contacter',
color: const Color(0xFFE17055),
onTap: () {},
),
],
),
const SizedBox(height: 24),
// Activités récentes
const Text(
'Activités Récentes',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Card(
child: Column(
children: [
_buildActivityItem(
icon: Icons.check_circle,
title: 'Participation confirmée',
subtitle: 'Assemblée Générale - Il y a 2h',
color: const Color(0xFF00B894),
),
const Divider(height: 1),
_buildActivityItem(
icon: Icons.payment,
title: 'Cotisation payée',
subtitle: 'Décembre 2024 - Il y a 1j',
color: const Color(0xFF0984E3),
),
const Divider(height: 1),
_buildActivityItem(
icon: Icons.event,
title: 'Événement créé',
subtitle: 'Sortie ski de fond - Il y a 3j',
color: const Color(0xFF00CEC9),
),
],
),
),
],
),
);
}
Widget _buildStatCard({
required IconData icon,
required String value,
required String title,
required Color color,
}) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildActionCard({
required IconData icon,
required String title,
required Color color,
required VoidCallback onTap,
}) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildActivityItem({
required IconData icon,
required String title,
required String subtitle,
required Color color,
}) {
return ListTile(
leading: CircleAvatar(
backgroundColor: color.withOpacity(0.1),
child: Icon(icon, color: color, size: 20),
),
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(subtitle),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
);
}
}

View File

@@ -0,0 +1,767 @@
/// Dashboard Consultant - Interface Limitée
/// Interface spécialisée pour consultants externes
library consultant_dashboard;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../../help/presentation/pages/help_support_page.dart';
import '../../../../notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
/// Dashboard pour Consultant Externe
class ConsultantDashboard extends StatefulWidget {
const ConsultantDashboard({super.key});
@override
State<ConsultantDashboard> createState() => _ConsultantDashboardState();
}
class _ConsultantDashboardState extends State<ConsultantDashboard> {
int _selectedIndex = 0;
final List<String> _consultantSections = [
'Mes Projets',
'Contacts',
'Profil',
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: Text(
'Consultant - ${_consultantSections[_selectedIndex]}',
style: const TextStyle(
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: Colors.white,
elevation: 2,
centerTitle: false,
actions: [
// Notifications consultant
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Color(0xFF6C5CE7)),
onPressed: () => _showConsultantNotifications(),
tooltip: 'Mes notifications',
),
// Menu consultant
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF6C5CE7)),
onSelected: (value) {
switch (value) {
case 'profile':
_editProfile();
break;
case 'contact':
_contactSupport();
break;
case 'help':
_showHelp();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'profile',
child: Row(
children: [
Icon(Icons.person, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Mon Profil'),
],
),
),
const PopupMenuItem(
value: 'contact',
child: Row(
children: [
Icon(Icons.support_agent, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Support'),
],
),
),
const PopupMenuItem(
value: 'help',
child: Row(
children: [
Icon(Icons.help, size: 20, color: Color(0xFF6C5CE7)),
SizedBox(width: 12),
Text('Aide'),
],
),
),
],
),
],
),
drawer: _buildConsultantDrawer(),
body: Stack(
children: [
_buildSelectedContent(),
// Navigation rapide consultant
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildConsultantQuickNavigation(),
),
],
),
);
}
/// Drawer de navigation consultant
Widget _buildConsultantDrawer() {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
Color(0xFF4834D4),
],
),
),
child: Column(
children: [
// Header consultant
Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.business_center,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sophie Martin',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Consultant IT',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// Menu de navigation
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _consultantSections.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
_getConsultantSectionIcon(index),
color: Colors.white,
size: 22,
),
title: Text(
_consultantSections[index],
style: TextStyle(
color: Colors.white,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
),
);
},
),
),
// Footer avec déconnexion
Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
icon: const Icon(Icons.logout, size: 16),
label: const Text('Déconnexion'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 40),
),
),
),
],
),
),
);
}
/// Icône pour chaque section consultant
IconData _getConsultantSectionIcon(int index) {
switch (index) {
case 0: return Icons.work;
case 1: return Icons.contacts;
case 2: return Icons.person;
default: return Icons.work;
}
}
/// Contenu de la section sélectionnée
Widget _buildSelectedContent() {
switch (_selectedIndex) {
case 0:
return _buildProjectsContent();
case 1:
return _buildContactsContent();
case 2:
return _buildProfileContent();
default:
return _buildProjectsContent();
}
}
/// Mes Projets - Vue des projets assignés
Widget _buildProjectsContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header projets
_buildProjectsHeader(),
const SizedBox(height: 20),
// Projets actifs
_buildActiveProjects(),
const SizedBox(height: 20),
// Tâches en cours
_buildCurrentTasks(),
const SizedBox(height: 20),
// Statistiques consultant
_buildConsultantStats(),
],
),
);
}
/// Placeholder pour les autres sections
Widget _buildContactsContent() {
return const Center(
child: Text(
'Contacts\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildProfileContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
const Text(
'Mon Profil',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _editProfile,
icon: const Icon(Icons.edit, size: 20),
label: const Text('Éditer mon profil'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
],
),
);
}
/// Header projets
Widget _buildProjectsHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const Row(
children: [
Icon(Icons.work, color: Color(0xFF6C5CE7), size: 24),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mes Projets Assignés',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'3 projets actifs',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
),
),
],
),
);
}
/// Projets actifs
Widget _buildActiveProjects() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Projets Actifs',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildProjectCard(
'Refonte Site Web',
'Développement frontend',
'75%',
const Color(0xFF00B894),
),
const SizedBox(height: 8),
_buildProjectCard(
'App Mobile',
'Interface utilisateur',
'45%',
const Color(0xFF0984E3),
),
const SizedBox(height: 8),
_buildProjectCard(
'API Backend',
'Architecture serveur',
'90%',
const Color(0xFFE17055),
),
],
);
}
/// Widget pour une carte de projet
Widget _buildProjectCard(String title, String description, String progress, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.folder, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
Text(
description,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
Text(
progress,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
LinearProgressIndicator(
value: double.parse(progress.replaceAll('%', '')) / 100,
backgroundColor: color.withOpacity(0.2),
valueColor: AlwaysStoppedAnimation<Color>(color),
),
],
),
);
}
/// Tâches en cours
Widget _buildCurrentTasks() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tâches du Jour',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
_buildTaskItem('Révision code frontend', true),
const SizedBox(height: 8),
_buildTaskItem('Réunion client 15h', false),
const SizedBox(height: 8),
_buildTaskItem('Tests unitaires', false),
],
),
),
],
);
}
/// Widget pour un élément de tâche
Widget _buildTaskItem(String task, bool completed) {
return Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: completed ? const Color(0xFF6C5CE7) : Colors.transparent,
border: Border.all(color: const Color(0xFF6C5CE7), width: 2),
borderRadius: BorderRadius.circular(4),
),
child: completed
? const Icon(Icons.check, color: Colors.white, size: 14)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
task,
style: TextStyle(
fontSize: 14,
decoration: completed ? TextDecoration.lineThrough : null,
color: completed ? Colors.grey[600] : Colors.black,
),
),
),
],
);
}
/// Statistiques consultant
Widget _buildConsultantStats() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Mes Statistiques',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard('Projets', '3', Icons.work, const Color(0xFF6C5CE7)),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard('Tâches', '12', Icons.task, const Color(0xFF00B894)),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard('Heures', '156h', Icons.schedule, const Color(0xFF0984E3)),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard('Éval.', '4.8/5', Icons.star, const Color(0xFFFDAB00)),
),
],
),
],
);
}
/// Widget pour une carte de statistique
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
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),
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
title,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
);
}
/// Navigation rapide consultant
Widget _buildConsultantQuickNavigation() {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(Icons.work, 'Projets', 0),
_buildNavItem(Icons.contacts, 'Contacts', 1),
_buildNavItem(Icons.person, 'Profil', 2),
],
),
);
}
/// Widget pour un élément de navigation
Widget _buildNavItem(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF6C5CE7).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 18,
color: isSelected
? const Color(0xFF6C5CE7)
: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? const Color(0xFF6C5CE7)
: Colors.grey[600],
),
),
],
),
),
);
}
// Méthodes d'action
void _showConsultantNotifications() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NotificationsPageWrapper(),
),
);
}
void _editProfile() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ProfilePageWrapper(),
),
);
}
void _contactSupport() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const HelpSupportPage(),
),
);
}
void _showHelp() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const HelpSupportPage(),
),
);
}
}

View File

@@ -0,0 +1,915 @@
/// Dashboard Gestionnaire RH - Interface Ressources Humaines
/// Outils spécialisés pour la gestion des employés et RH
library hr_manager_dashboard;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../authentication/presentation/bloc/auth_bloc.dart';
/// Dashboard spécialisé pour Gestionnaire RH
///
/// Fonctionnalités RH :
/// - Gestion des employés
/// - Recrutement et onboarding
/// - Évaluations de performance
/// - Congés et absences
/// - Reporting RH
/// - Formation et développement
class HRManagerDashboard extends StatefulWidget {
const HRManagerDashboard({super.key});
@override
State<HRManagerDashboard> createState() => _HRManagerDashboardState();
}
class _HRManagerDashboardState extends State<HRManagerDashboard>
with TickerProviderStateMixin {
late TabController _tabController;
int _selectedIndex = 0;
final List<String> _hrSections = [
'Vue d\'ensemble',
'Employés',
'Recrutement',
'Évaluations',
'Congés',
'Formation',
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _hrSections.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
appBar: AppBar(
title: Text(
'RH Manager - ${_hrSections[_selectedIndex]}',
style: const TextStyle(
color: Color(0xFF00B894),
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
backgroundColor: Colors.white,
elevation: 2,
centerTitle: false,
actions: [
// Recherche employés
IconButton(
icon: const Icon(Icons.search, color: Color(0xFF00B894)),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MembersPageWrapper(),
),
);
},
tooltip: 'Rechercher employés',
),
// Notifications RH
IconButton(
icon: const Icon(Icons.notifications_outlined, color: Color(0xFF00B894)),
onPressed: () => _showHRNotifications(),
tooltip: 'Notifications RH',
),
// Menu RH
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Color(0xFF00B894)),
onSelected: (value) {
switch (value) {
case 'reports':
_generateHRReports();
break;
case 'settings':
_openHRSettings();
break;
case 'export':
_exportHRData();
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'reports',
child: Row(
children: [
Icon(Icons.assessment, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Rapports RH'),
],
),
),
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Paramètres RH'),
],
),
),
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download, size: 20, color: Color(0xFF00B894)),
SizedBox(width: 12),
Text('Exporter données'),
],
),
),
],
),
],
),
drawer: _buildHRDrawer(),
body: Stack(
children: [
_buildSelectedContent(),
// Navigation rapide RH
Positioned(
bottom: 20,
left: 20,
right: 20,
child: _buildHRQuickNavigation(),
),
],
),
);
}
/// Drawer de navigation RH
Widget _buildHRDrawer() {
return Drawer(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF00B894),
Color(0xFF00A085),
Color(0xFF008B75),
],
),
),
child: Column(
children: [
// Header RH
Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gestionnaire RH',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Ressources Humaines',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
),
),
],
),
),
// Menu de navigation
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _hrSections.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected
? Colors.white.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(
_getHRSectionIcon(index),
color: Colors.white,
size: 22,
),
title: Text(
_hrSections[index],
style: TextStyle(
color: Colors.white,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
onTap: () {
setState(() => _selectedIndex = index);
Navigator.pop(context);
},
),
);
},
),
),
// Footer avec déconnexion
Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
icon: const Icon(Icons.logout, size: 16),
label: const Text('Déconnexion'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 40),
),
),
),
],
),
),
);
}
/// Icône pour chaque section RH
IconData _getHRSectionIcon(int index) {
switch (index) {
case 0: return Icons.dashboard;
case 1: return Icons.people;
case 2: return Icons.person_add;
case 3: return Icons.star_rate;
case 4: return Icons.event_busy;
case 5: return Icons.school;
default: return Icons.dashboard;
}
}
/// Contenu de la section sélectionnée
Widget _buildSelectedContent() {
switch (_selectedIndex) {
case 0:
return _buildOverviewContent();
case 1:
return _buildEmployeesContent();
case 2:
return _buildRecruitmentContent();
case 3:
return _buildEvaluationsContent();
case 4:
return _buildLeavesContent();
case 5:
return _buildTrainingContent();
default:
return _buildOverviewContent();
}
}
/// Vue d'ensemble RH
Widget _buildOverviewContent() {
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec statut RH
_buildHRStatusHeader(),
const SizedBox(height: 20),
// KPIs RH
_buildHRKPIsSection(),
const SizedBox(height: 20),
// Actions rapides RH
_buildHRQuickActions(),
const SizedBox(height: 20),
// Alertes RH importantes
_buildHRAlerts(),
],
),
);
}
/// Placeholder pour les autres sections
Widget _buildEmployeesContent() {
return const Center(
child: Text(
'Gestion des Employés\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildRecruitmentContent() {
return const Center(
child: Text(
'Recrutement\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildEvaluationsContent() {
return const Center(
child: Text(
'Évaluations\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildLeavesContent() {
return const Center(
child: Text(
'Congés et Absences\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
Widget _buildTrainingContent() {
return const Center(
child: Text(
'Formation\n(En développement)',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
);
}
/// Header avec statut RH
Widget _buildHRStatusHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF00B894), Color(0xFF00A085)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF00B894).withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Département RH Actif',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Dernière sync: ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.people,
color: Colors.white,
size: 28,
),
),
],
),
);
}
/// Section KPIs RH
Widget _buildHRKPIsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Indicateurs RH',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHRKPICard(
'Employés Actifs',
'247',
'+12 ce mois',
Icons.people,
const Color(0xFF00B894),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHRKPICard(
'Candidatures',
'34',
'+8 cette semaine',
Icons.person_add,
const Color(0xFF0984E3),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildHRKPICard(
'En Congé',
'18',
'7.3% de l\'effectif',
Icons.event_busy,
const Color(0xFFFDAB00),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildHRKPICard(
'Évaluations',
'156',
'89% complétées',
Icons.star_rate,
const Color(0xFFE17055),
),
),
],
),
],
);
}
/// Widget pour une carte KPI RH
Widget _buildHRKPICard(
String title,
String value,
String subtitle,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const Spacer(),
],
),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
],
),
);
}
/// Actions rapides RH
Widget _buildHRQuickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions Rapides',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.5,
children: [
_buildHRActionCard(
'Nouveau Employé',
Icons.person_add,
const Color(0xFF00B894),
() => _addNewEmployee(),
),
_buildHRActionCard(
'Demandes Congés',
Icons.event_busy,
const Color(0xFFFDAB00),
() => _viewLeaveRequests(),
),
_buildHRActionCard(
'Évaluations',
Icons.star_rate,
const Color(0xFFE17055),
() => _viewEvaluations(),
),
_buildHRActionCard(
'Recrutement',
Icons.work,
const Color(0xFF0984E3),
() => _viewRecruitment(),
),
],
),
],
);
}
/// Widget pour une action RH
Widget _buildHRActionCard(
String title,
IconData icon,
Color color,
VoidCallback onTap,
) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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,
),
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
/// Alertes RH importantes
Widget _buildHRAlerts() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Alertes Importantes',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildHRAlertItem(
'Évaluations en retard',
'12 évaluations annuelles à finaliser',
Icons.warning,
const Color(0xFFE17055),
),
const SizedBox(height: 8),
_buildHRAlertItem(
'Congés à approuver',
'5 demandes de congé en attente',
Icons.pending_actions,
const Color(0xFFFDAB00),
),
const SizedBox(height: 8),
_buildHRAlertItem(
'Nouveaux candidats',
'8 candidatures reçues cette semaine',
Icons.person_add,
const Color(0xFF0984E3),
),
],
);
}
/// Widget pour un élément d'alerte RH
Widget _buildHRAlertItem(
String title,
String message,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: color,
),
),
Text(
message,
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
],
),
),
],
),
);
}
/// Navigation rapide RH
Widget _buildHRQuickNavigation() {
return Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 5),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildHRNavItem(Icons.dashboard, 'Vue', 0),
_buildHRNavItem(Icons.people, 'Employés', 1),
_buildHRNavItem(Icons.person_add, 'Recrutement', 2),
_buildHRNavItem(Icons.star_rate, 'Évaluations', 3),
_buildHRNavItem(Icons.event_busy, 'Congés', 4),
],
),
);
}
/// Widget pour un élément de navigation RH
Widget _buildHRNavItem(IconData icon, String label, int index) {
final isSelected = _selectedIndex == index;
return GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF00B894).withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
size: 18,
color: isSelected
? const Color(0xFF00B894)
: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? const Color(0xFF00B894)
: Colors.grey[600],
),
),
],
),
),
);
}
// Méthodes d'action
void _showHRNotifications() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const NotificationsPageWrapper(),
),
);
}
void _generateHRReports() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
}
void _openHRSettings() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
);
}
void _exportHRData() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
}
void _addNewEmployee() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Ajouter employé - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF00B894),
),
);
}
void _viewLeaveRequests() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Demandes de congé - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFFFDAB00),
),
);
}
void _viewEvaluations() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Évaluations - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFFE17055),
),
);
}
void _viewRecruitment() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recrutement - Fonctionnalité à implémenter'),
backgroundColor: Color(0xFF0984E3),
),
);
}
}

View File

@@ -0,0 +1,230 @@
/// Dashboard Modérateur - Management Hub Focalisé
/// Outils de modération et gestion partielle
library moderator_dashboard;
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Management Hub pour Modérateur
class ModeratorDashboard extends StatelessWidget {
const ModeratorDashboard({super.key});
@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,
),
),
child: const Center(
child: Icon(Icons.manage_accounts, color: Colors.white, size: 60),
),
),
),
),
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(),
],
),
),
),
],
),
);
}
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,11 @@
/// Export de tous les dashboards spécifiques par rôle
/// Facilite l'importation des dashboards dans l'application
library role_dashboards;
// Dashboards spécifiques par rôle
export 'super_admin_dashboard.dart';
export 'org_admin_dashboard.dart';
export 'moderator_dashboard.dart';
export 'active_member_dashboard.dart';
export 'simple_member_dashboard.dart';
export 'visitor_dashboard.dart';

View File

@@ -0,0 +1,360 @@
/// Dashboard Membre Simple - Personal Space Minimaliste
/// Interface simplifiée pour accès basique
library simple_member_dashboard;
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Personal Space pour Membre Simple
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,
),
),
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),
),
],
),
child: Row(
children: [
const CircleAvatar(
radius: 35,
backgroundColor: Color(0xFF00CEC9),
child: Icon(Icons.person, color: Colors.white, size: 35),
),
const SizedBox(width: SpacingTokens.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pierre Dupont',
style: TypographyTokens.headlineMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
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,
),
),
),
],
),
),
],
),
);
}
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',
),
],
),
],
);
}
}

View File

@@ -0,0 +1,554 @@
/// 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';
/// Dashboard Landing Experience pour Visiteur
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),
),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Stack(
children: [
// Motif d'accueil
Positioned.fill(
child: CustomPaint(
painter: _WelcomePatternPainter(),
),
),
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,
),
),
],
),
),
],
),
),
),
),
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(),
],
),
),
),
],
),
);
}
Widget _buildWelcomeMessage() {
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(RadiusTokens.lg),
),
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,
),
),
),
],
),
const SizedBox(height: SpacingTokens.md),
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),
),
),
],
),
);
}
Widget _buildAboutOrganization() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'À Propos de Nous',
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: [
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)),
],
),
),
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 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,410 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.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';
/// Widget de graphique pour le dashboard
class DashboardChartWidget extends StatelessWidget {
final String title;
final DashboardChartType chartType;
final double height;
const DashboardChartWidget({
super.key,
required this.title,
required this.chartType,
this.height = 200,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
SizedBox(
height: height,
child: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return _buildLoadingChart();
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildChart(data);
} else if (state is DashboardError) {
return _buildErrorChart();
}
return _buildEmptyChart();
},
),
),
],
),
);
}
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,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Text(
title,
style: DashboardTheme.titleMedium,
),
),
],
);
}
Widget _buildChart(DashboardEntity data) {
switch (chartType) {
case DashboardChartType.memberActivity:
return _buildMemberActivityChart(data.stats);
case DashboardChartType.contributionTrend:
return _buildContributionTrendChart(data.stats);
case DashboardChartType.eventParticipation:
return _buildEventParticipationChart(data.upcomingEvents);
case DashboardChartType.monthlyGrowth:
return _buildMonthlyGrowthChart(data.stats);
}
}
Widget _buildMemberActivityChart(DashboardStatsEntity stats) {
return PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: 40,
sections: [
PieChartSectionData(
color: DashboardTheme.success,
value: stats.activeMembers.toDouble(),
title: '${stats.activeMembers}',
radius: 50,
titleStyle: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
),
),
PieChartSectionData(
color: DashboardTheme.grey300,
value: (stats.totalMembers - stats.activeMembers).toDouble(),
title: '${stats.totalMembers - stats.activeMembers}',
radius: 45,
titleStyle: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey700,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildContributionTrendChart(DashboardStatsEntity stats) {
return LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: stats.totalContributionAmount / 4,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: DashboardTheme.grey200,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (double value, TitleMeta meta) {
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'];
if (value.toInt() >= 0 && value.toInt() < months.length) {
return Text(
months[value.toInt()],
style: DashboardTheme.bodySmall,
);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: stats.totalContributionAmount / 4,
reservedSize: 60,
getTitlesWidget: (double value, TitleMeta meta) {
return Text(
'${(value / 1000).toStringAsFixed(0)}K',
style: DashboardTheme.bodySmall,
);
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 5,
minY: 0,
maxY: stats.totalContributionAmount,
lineBarsData: [
LineChartBarData(
spots: _generateContributionSpots(stats),
isCurved: true,
gradient: const LinearGradient(
colors: [
DashboardTheme.tealBlue,
DashboardTheme.royalBlue,
],
),
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
DashboardTheme.tealBlue.withOpacity(0.3),
DashboardTheme.royalBlue.withOpacity(0.1),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
);
}
Widget _buildEventParticipationChart(List<UpcomingEventEntity> events) {
if (events.isEmpty) {
return _buildEmptyChart();
}
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: events.map((e) => e.maxParticipants).reduce((a, b) => a > b ? a : b).toDouble(),
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (double value, TitleMeta meta) {
if (value.toInt() < events.length) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
events[value.toInt()].title.length > 8
? '${events[value.toInt()].title.substring(0, 8)}...'
: events[value.toInt()].title,
style: DashboardTheme.bodySmall,
textAlign: TextAlign.center,
),
);
}
return const Text('');
},
reservedSize: 40,
),
),
leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
barGroups: events.asMap().entries.map((entry) {
final index = entry.key;
final event = entry.value;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: event.currentParticipants.toDouble(),
color: event.isFull
? DashboardTheme.error
: event.isAlmostFull
? DashboardTheme.warning
: DashboardTheme.success,
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
],
);
}).toList(),
),
);
}
Widget _buildMonthlyGrowthChart(DashboardStatsEntity stats) {
return LineChart(
LineChartData(
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(show: false),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 11,
minY: -5,
maxY: 20,
lineBarsData: [
LineChartBarData(
spots: _generateGrowthSpots(stats.monthlyGrowth),
isCurved: true,
color: stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: (stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error)
.withOpacity(0.2),
),
),
],
),
);
}
List<FlSpot> _generateContributionSpots(DashboardStatsEntity stats) {
final baseAmount = stats.totalContributionAmount / 6;
return [
FlSpot(0, baseAmount * 0.8),
FlSpot(1, baseAmount * 1.2),
FlSpot(2, baseAmount * 0.9),
FlSpot(3, baseAmount * 1.5),
FlSpot(4, baseAmount * 1.1),
FlSpot(5, baseAmount * 1.3),
];
}
List<FlSpot> _generateGrowthSpots(double currentGrowth) {
final baseGrowth = currentGrowth;
return List.generate(12, (index) {
final variation = (index % 3 - 1) * 2.0;
return FlSpot(index.toDouble(), baseGrowth + variation);
});
}
Widget _buildLoadingChart() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.grey100,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: const Center(
child: CircularProgressIndicator(
color: DashboardTheme.royalBlue,
),
),
);
}
Widget _buildErrorChart() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
),
),
],
),
),
);
}
Widget _buildEmptyChart() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.grey50,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.bar_chart,
color: DashboardTheme.grey400,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Aucune donnée',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
),
],
),
),
);
}
IconData _getChartIcon() {
switch (chartType) {
case DashboardChartType.memberActivity:
return Icons.pie_chart;
case DashboardChartType.contributionTrend:
return Icons.trending_up;
case DashboardChartType.eventParticipation:
return Icons.bar_chart;
case DashboardChartType.monthlyGrowth:
return Icons.show_chart;
}
}
}
enum DashboardChartType {
memberActivity,
contributionTrend,
eventParticipation,
monthlyGrowth,
}

View File

@@ -0,0 +1,463 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
/// Widget réutilisable pour afficher un élément d'activité
///
/// Composant standardisé pour les listes d'activités récentes,
/// notifications, historiques, etc.
///
/// REFACTORISÉ pour utiliser le Design System UnionFlow.
class ActivityItem extends StatelessWidget {
/// Titre principal de l'activité
final String title;
/// Description ou détails de l'activité
final String? description;
/// Horodatage de l'activité
final String timestamp;
/// Icône représentative de l'activité
final IconData? icon;
/// Couleur thématique de l'activité
final Color? color;
/// Type d'activité pour le style automatique
final ActivityType? type;
/// Callback lors du tap sur l'élément
final VoidCallback? onTap;
/// Style de l'élément d'activité
final ActivityItemStyle style;
/// Afficher ou non l'indicateur de statut
final bool showStatusIndicator;
const ActivityItem({
super.key,
required this.title,
this.description,
required this.timestamp,
this.icon,
this.color,
this.type,
this.onTap,
this.style = ActivityItemStyle.normal,
this.showStatusIndicator = true,
});
/// Constructeur pour une activité système
const ActivityItem.system({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.settings,
color = ColorTokens.primary,
type = ActivityType.system,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
/// Constructeur pour une activité utilisateur
const ActivityItem.user({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.person,
color = ColorTokens.success,
type = ActivityType.user,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
/// Constructeur pour une alerte
const ActivityItem.alert({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.warning,
color = ColorTokens.warning,
type = ActivityType.alert,
style = ActivityItemStyle.alert,
showStatusIndicator = true;
/// Constructeur pour une erreur
const ActivityItem.error({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.error,
color = Colors.red,
type = ActivityType.error,
style = ActivityItemStyle.alert,
showStatusIndicator = true;
/// Constructeur pour une activité de succès
const ActivityItem.success({
super.key,
required this.title,
this.description,
required this.timestamp,
this.onTap,
}) : icon = Icons.check_circle,
color = const Color(0xFF00B894),
type = ActivityType.success,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
@override
Widget build(BuildContext context) {
final effectiveColor = _getEffectiveColor();
final effectiveIcon = _getEffectiveIcon();
return GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: _getPadding(),
decoration: _getDecoration(effectiveColor),
child: _buildContent(effectiveColor, effectiveIcon),
),
);
}
/// Contenu principal de l'élément
Widget _buildContent(Color effectiveColor, IconData effectiveIcon) {
switch (style) {
case ActivityItemStyle.minimal:
return _buildMinimalContent(effectiveColor, effectiveIcon);
case ActivityItemStyle.normal:
return _buildNormalContent(effectiveColor, effectiveIcon);
case ActivityItemStyle.detailed:
return _buildDetailedContent(effectiveColor, effectiveIcon);
case ActivityItemStyle.alert:
return _buildAlertContent(effectiveColor, effectiveIcon);
}
}
/// Contenu minimal (ligne simple)
Widget _buildMinimalContent(Color effectiveColor, IconData effectiveIcon) {
return Row(
children: [
if (showStatusIndicator)
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: effectiveColor,
shape: BoxShape.circle,
),
),
if (showStatusIndicator) const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
Text(
timestamp,
style: const TextStyle(
color: Colors.grey,
fontSize: 10,
),
),
],
);
}
/// Contenu normal avec icône
Widget _buildNormalContent(Color effectiveColor, IconData effectiveIcon) {
return Row(
children: [
if (showStatusIndicator) ...[
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
effectiveIcon,
color: effectiveColor,
size: 16,
),
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
if (description != null) ...[
const SizedBox(height: 2),
Text(
description!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
const SizedBox(width: 8),
Text(
timestamp,
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
],
);
}
/// Contenu détaillé avec plus d'informations
Widget _buildDetailedContent(Color effectiveColor, IconData effectiveIcon) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
effectiveIcon,
color: effectiveColor,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
),
Text(
timestamp,
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
if (description != null) ...[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 42),
child: Text(
description!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.4,
),
),
),
],
],
);
}
/// Contenu pour les alertes avec style spécial
Widget _buildAlertContent(Color effectiveColor, IconData effectiveIcon) {
return Row(
children: [
Icon(
effectiveIcon,
color: effectiveColor,
size: 18,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: effectiveColor,
),
),
if (description != null) ...[
const SizedBox(height: 2),
Text(
description!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
const SizedBox(width: 8),
Text(
timestamp,
style: TextStyle(
color: Colors.grey[500],
fontSize: 11,
),
),
],
);
}
/// Couleur effective selon le type
Color _getEffectiveColor() {
if (color != null) return color!;
switch (type) {
case ActivityType.system:
return ColorTokens.primary;
case ActivityType.user:
return ColorTokens.success;
case ActivityType.organization:
return ColorTokens.info;
case ActivityType.event:
return ColorTokens.secondary;
case ActivityType.alert:
return ColorTokens.warning;
case ActivityType.error:
return ColorTokens.error;
case ActivityType.success:
return ColorTokens.success;
case null:
return ColorTokens.primary;
}
}
/// Icône effective selon le type
IconData _getEffectiveIcon() {
if (icon != null) return icon!;
switch (type) {
case ActivityType.system:
return Icons.settings;
case ActivityType.user:
return Icons.person;
case ActivityType.organization:
return Icons.business;
case ActivityType.event:
return Icons.event;
case ActivityType.alert:
return Icons.warning;
case ActivityType.error:
return Icons.error;
case ActivityType.success:
return Icons.check_circle;
case null:
return Icons.circle;
}
}
/// Padding selon le style
EdgeInsets _getPadding() {
switch (style) {
case ActivityItemStyle.minimal:
return const EdgeInsets.symmetric(vertical: 4, horizontal: 8);
case ActivityItemStyle.normal:
return const EdgeInsets.all(8);
case ActivityItemStyle.detailed:
return const EdgeInsets.all(12);
case ActivityItemStyle.alert:
return const EdgeInsets.all(10);
}
}
/// Décoration selon le style
BoxDecoration _getDecoration(Color effectiveColor) {
switch (style) {
case ActivityItemStyle.minimal:
return const BoxDecoration();
case ActivityItemStyle.normal:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
);
case ActivityItemStyle.detailed:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
);
case ActivityItemStyle.alert:
return BoxDecoration(
color: effectiveColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: effectiveColor.withOpacity(0.2),
width: 1,
),
);
}
}
}
/// Types d'activité
enum ActivityType {
system,
user,
organization,
event,
alert,
error,
success,
}
/// Styles d'élément d'activité
enum ActivityItemStyle {
minimal,
normal,
detailed,
alert,
}

View File

@@ -0,0 +1,302 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
/// Widget réutilisable pour les en-têtes de section
///
/// Composant standardisé pour tous les titres de section dans les dashboards
/// avec support pour actions, sous-titres et styles personnalisés.
///
/// REFACTORISÉ pour utiliser le Design System UnionFlow.
class SectionHeader extends StatelessWidget {
/// Titre principal de la section
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Widget d'action à droite (bouton, icône, etc.)
final Widget? action;
/// Icône optionnelle à gauche du titre
final IconData? icon;
/// Couleur du titre et de l'icône
final Color? color;
/// Taille du titre
final double? fontSize;
/// Style de l'en-tête
final SectionHeaderStyle style;
/// Espacement en bas de l'en-tête
final double bottomSpacing;
const SectionHeader({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
this.color,
this.fontSize,
this.style = SectionHeaderStyle.normal,
this.bottomSpacing = 12,
});
/// Constructeur pour un en-tête principal
const SectionHeader.primary({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
}) : color = ColorTokens.primary,
fontSize = 20,
style = SectionHeaderStyle.primary,
bottomSpacing = 16;
/// Constructeur pour un en-tête de section
const SectionHeader.section({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
}) : color = ColorTokens.primary,
fontSize = 16,
style = SectionHeaderStyle.normal,
bottomSpacing = 12;
/// Constructeur pour un en-tête de sous-section
const SectionHeader.subsection({
super.key,
required this.title,
this.subtitle,
this.action,
this.icon,
}) : color = const Color(0xFF374151),
fontSize = 14,
style = SectionHeaderStyle.minimal,
bottomSpacing = 8;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: bottomSpacing),
child: _buildContent(),
);
}
Widget _buildContent() {
switch (style) {
case SectionHeaderStyle.primary:
return _buildPrimaryHeader();
case SectionHeaderStyle.normal:
return _buildNormalHeader();
case SectionHeaderStyle.minimal:
return _buildMinimalHeader();
case SectionHeaderStyle.card:
return _buildCardHeader();
}
}
/// En-tête principal avec fond coloré
Widget _buildPrimaryHeader() {
final effectiveColor = color ?? ColorTokens.primary;
return Container(
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
effectiveColor,
effectiveColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.primary,
),
child: Row(
children: [
if (icon != null) ...[
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: fontSize ?? 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
],
),
),
if (action != null) action!,
],
),
);
}
/// En-tête normal avec icône et action
Widget _buildNormalHeader() {
return Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: color ?? ColorTokens.primary,
size: 20,
),
const SizedBox(width: SpacingTokens.md),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.bold,
color: color ?? ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
if (action != null) action!,
],
);
}
/// En-tête minimal simple
Widget _buildMinimalHeader() {
return Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: color ?? const Color(0xFF374151),
size: 16,
),
const SizedBox(width: 6),
],
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: fontSize ?? 14,
fontWeight: FontWeight.w600,
color: color ?? const Color(0xFF374151),
),
),
),
if (action != null) action!,
],
);
}
/// En-tête avec fond de carte
Widget _buildCardHeader() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
if (icon != null) ...[
Icon(
icon,
color: color ?? ColorTokens.primary,
size: 20,
),
const SizedBox(width: SpacingTokens.md),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.bold,
color: color ?? ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
if (action != null) action!,
],
),
);
}
}
/// Énumération des styles d'en-tête
enum SectionHeaderStyle {
primary,
normal,
minimal,
card,
}

View File

@@ -0,0 +1,292 @@
import 'package:flutter/material.dart';
/// Widget réutilisable pour afficher une carte de statistique
///
/// Composant générique utilisé dans tous les dashboards pour afficher
/// des métriques avec icône, valeur, titre et sous-titre.
class StatCard extends StatelessWidget {
/// Titre principal de la statistique
final String title;
/// Valeur numérique ou textuelle à afficher
final String value;
/// Sous-titre ou description complémentaire
final String subtitle;
/// Icône représentative de la métrique
final IconData icon;
/// Couleur thématique de la carte
final Color color;
/// Callback optionnel lors du tap sur la carte
final VoidCallback? onTap;
/// Taille de la carte (compact, normal, large)
final StatCardSize size;
/// Style de la carte (minimal, elevated, outlined)
final StatCardStyle style;
const StatCard({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
this.size = StatCardSize.normal,
this.style = StatCardStyle.elevated,
});
/// Constructeur pour une carte KPI simplifiée
const StatCard.kpi({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
}) : size = StatCardSize.compact,
style = StatCardStyle.elevated;
/// Constructeur pour une carte de métrique système
const StatCard.metric({
super.key,
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
}) : size = StatCardSize.normal,
style = StatCardStyle.minimal;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: _getPadding(),
decoration: _getDecoration(),
child: _buildContent(),
),
);
}
/// Contenu principal de la carte
Widget _buildContent() {
switch (size) {
case StatCardSize.compact:
return _buildCompactContent();
case StatCardSize.normal:
return _buildNormalContent();
case StatCardSize.large:
return _buildLargeContent();
}
}
/// Contenu compact pour les KPIs
Widget _buildCompactContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color, size: 20),
const Spacer(),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 18,
),
),
],
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
fontSize: 12,
),
),
Text(
subtitle,
style: const TextStyle(
color: Colors.grey,
fontSize: 10,
),
),
],
);
}
/// Contenu normal pour les métriques
Widget _buildNormalContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 20,
),
),
if (subtitle.isNotEmpty)
Text(
subtitle,
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
),
],
),
],
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
fontSize: 14,
),
),
],
);
}
/// Contenu large pour les dashboards principaux
Widget _buildLargeContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 24,
),
),
if (subtitle.isNotEmpty)
Text(
subtitle,
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
],
),
const SizedBox(height: 16),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
fontSize: 16,
),
),
],
);
}
/// Padding selon la taille
EdgeInsets _getPadding() {
switch (size) {
case StatCardSize.compact:
return const EdgeInsets.all(8);
case StatCardSize.normal:
return const EdgeInsets.all(12);
case StatCardSize.large:
return const EdgeInsets.all(16);
}
}
/// Décoration selon le style
BoxDecoration _getDecoration() {
switch (style) {
case StatCardStyle.minimal:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
);
case StatCardStyle.elevated:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
);
case StatCardStyle.outlined:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
);
}
}
}
/// Énumération des tailles de carte
enum StatCardSize {
compact,
normal,
large,
}
/// Énumération des styles de carte
enum StatCardStyle {
minimal,
elevated,
outlined,
}

View File

@@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import '../../../../../../shared/design_system/unionflow_design_system.dart';
/// Carte de performance système réutilisable
///
/// Widget spécialisé pour afficher les métriques de performance
/// avec barres de progression et indicateurs colorés.
///
/// REFACTORISÉ pour utiliser le Design System UnionFlow.
class PerformanceCard extends StatelessWidget {
/// Titre de la carte
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Liste des métriques de performance
final List<PerformanceMetric> metrics;
/// Style de la carte
final PerformanceCardStyle style;
/// Callback lors du tap sur la carte
final VoidCallback? onTap;
/// Afficher ou non les valeurs numériques
final bool showValues;
/// Afficher ou non les barres de progression
final bool showProgressBars;
const PerformanceCard({
super.key,
required this.title,
this.subtitle,
required this.metrics,
this.style = PerformanceCardStyle.elevated,
this.onTap,
this.showValues = true,
this.showProgressBars = true,
});
/// Constructeur pour les métriques serveur
const PerformanceCard.server({
super.key,
this.onTap,
}) : title = 'Performance Serveur',
subtitle = 'Métriques temps réel',
metrics = const [
PerformanceMetric(
label: 'CPU',
value: 67.3,
unit: '%',
color: ColorTokens.warning,
threshold: 80,
),
PerformanceMetric(
label: 'RAM',
value: 78.5,
unit: '%',
color: ColorTokens.info,
threshold: 85,
),
PerformanceMetric(
label: 'Disque',
value: 45.2,
unit: '%',
color: ColorTokens.success,
threshold: 90,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = true;
/// Constructeur pour les métriques réseau
const PerformanceCard.network({
super.key,
this.onTap,
}) : title = 'Performance Réseau',
subtitle = 'Métriques temps réel',
metrics = const [
PerformanceMetric(
label: 'Latence',
value: 12.0,
unit: 'ms',
color: ColorTokens.success,
threshold: 100.0,
),
PerformanceMetric(
label: 'Débit',
value: 85.0,
unit: 'Mbps',
color: ColorTokens.primary,
threshold: 100.0,
),
PerformanceMetric(
label: 'Paquets perdus',
value: 0.2,
unit: '%',
color: ColorTokens.secondary,
threshold: 5.0,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = true;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: UFCard(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: SpacingTokens.lg),
_buildMetrics(),
],
),
),
);
}
/// En-tête de la carte
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.titleMedium.copyWith(
fontWeight: FontWeight.bold,
color: ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
],
);
}
/// Construction des métriques
Widget _buildMetrics() {
return Column(
children: metrics.map((metric) => Padding(
padding: const EdgeInsets.only(bottom: SpacingTokens.md),
child: _buildMetricRow(metric),
)).toList(),
);
}
/// Ligne de métrique
Widget _buildMetricRow(PerformanceMetric metric) {
final isWarning = metric.value > metric.threshold * 0.8;
final isCritical = metric.value > metric.threshold;
Color effectiveColor = metric.color;
if (isCritical) {
effectiveColor = ColorTokens.error;
} else if (isWarning) {
effectiveColor = ColorTokens.warning;
}
return Column(
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: effectiveColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: SpacingTokens.md),
Text(
metric.label,
style: TypographyTokens.labelMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
if (showValues)
Text(
'${metric.value.toStringAsFixed(1)}${metric.unit}',
style: TypographyTokens.labelMedium.copyWith(
color: effectiveColor,
fontWeight: FontWeight.w600,
),
),
],
),
if (showProgressBars) ...[
const SizedBox(height: SpacingTokens.xs),
_buildProgressBar(metric, effectiveColor),
],
],
);
}
/// Barre de progression
Widget _buildProgressBar(PerformanceMetric metric, Color color) {
final progress = (metric.value / metric.threshold).clamp(0.0, 1.0);
return Container(
height: 4,
decoration: BoxDecoration(
color: ColorTokens.surfaceVariant,
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
),
),
);
}
}
/// Modèle de données pour une métrique de performance
class PerformanceMetric {
final String label;
final double value;
final String unit;
final Color color;
final double threshold;
const PerformanceMetric({
required this.label,
required this.value,
required this.unit,
required this.color,
required this.threshold,
});
}
/// Styles de carte de performance
enum PerformanceCardStyle {
elevated,
outlined,
minimal,
}

View File

@@ -0,0 +1,365 @@
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 '../../../../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';
/// Widget des activités récentes connecté au backend
class ConnectedRecentActivities extends StatelessWidget {
final int maxItems;
final VoidCallback? onSeeAll;
const ConnectedRecentActivities({
super.key,
this.maxItems = 5,
this.onSeeAll,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, 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 _buildActivitiesList(data.recentActivities);
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
return _buildEmptyState();
},
),
],
),
);
}
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 SizedBox(width: DashboardTheme.spacing12),
const Expanded(
child: Text(
'Activités récentes',
style: DashboardTheme.titleMedium,
),
),
if (onSeeAll != null)
TextButton(
onPressed: onSeeAll,
child: Text(
'Voir tout',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildActivitiesList(List<RecentActivityEntity> activities) {
if (activities.isEmpty) {
return _buildEmptyState();
}
final displayActivities = activities.take(maxItems).toList();
return Column(
children: displayActivities.asMap().entries.map((entry) {
final index = entry.key;
final activity = entry.value;
final isLast = index == displayActivities.length - 1;
return Column(
children: [
_buildActivityItem(activity),
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
],
);
}).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),
),
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(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
activity.description,
style: DashboardTheme.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: DashboardTheme.spacing4),
Row(
children: [
Text(
activity.userName,
style: DashboardTheme.bodySmall.copyWith(
fontWeight: FontWeight.w500,
color: DashboardTheme.royalBlue,
),
),
Text(
'${activity.timeAgo}',
style: DashboardTheme.bodySmall,
),
],
),
],
),
),
// Action button si disponible
if (activity.hasAction)
IconButton(
onPressed: () => _navigateForActivity(context, activity),
icon: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: DashboardTheme.grey400,
),
),
],
);
}
void _navigateForActivity(BuildContext context, RecentActivityEntity activity) {
final type = activity.type.toLowerCase();
Widget? page;
if (type.contains('event') || type.contains('evenement')) {
page = const EventsPageWrapper();
} else if (type.contains('member') || type.contains('membre')) {
page = const MembersPageWrapper();
} else if (type.contains('adhesion') || type.contains('adhésion')) {
page = const AdhesionsPageWrapper();
} else if (type.contains('demande') || type.contains('solidarite') || type.contains('aide')) {
page = const DemandesAidePageWrapper();
}
if (page != null) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => page!));
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(activity.title)),
);
}
}
Widget _buildLoadingList() {
return Column(
children: List.generate(3, (index) => Column(
children: [
_buildLoadingItem(),
if (index < 2) const SizedBox(height: DashboardTheme.spacing12),
],
)),
);
}
Widget _buildLoadingItem() {
return Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(20),
),
),
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: 200,
decoration: BoxDecoration(
color: DashboardTheme.grey100,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: DashboardTheme.spacing4),
Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: DashboardTheme.grey100,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
);
}
Widget _buildErrorState(String message) {
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,
),
],
),
);
}
Widget _buildEmptyState() {
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,
),
],
),
);
}
IconData _getActivityIcon(String type) {
switch (type.toLowerCase()) {
case 'member':
return Icons.person_add;
case 'event':
return Icons.event;
case 'contribution':
return Icons.payment;
case 'organization':
return Icons.business;
case 'system':
return Icons.settings;
default:
return Icons.notifications;
}
}
Color _getActivityColor(String type) {
switch (type.toLowerCase()) {
case 'member':
return DashboardTheme.success;
case 'event':
return DashboardTheme.info;
case 'contribution':
return DashboardTheme.tealBlue;
case 'organization':
return DashboardTheme.royalBlue;
case 'system':
return DashboardTheme.warning;
default:
return DashboardTheme.grey500;
}
}
}

View File

@@ -0,0 +1,203 @@
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';
/// Widget de carte de statistiques connecté au backend
class ConnectedStatsCard extends StatelessWidget {
final String title;
final IconData icon;
final String Function(DashboardStatsEntity) valueExtractor;
final String? Function(DashboardStatsEntity)? subtitleExtractor;
final Color? customColor;
final VoidCallback? onTap;
const ConnectedStatsCard({
super.key,
required this.title,
required this.icon,
required this.valueExtractor,
this.subtitleExtractor,
this.customColor,
this.onTap,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return _buildLoadingCard();
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildDataCard(data.stats);
} else if (state is DashboardError) {
return _buildErrorCard(state.message);
}
return _buildLoadingCard();
},
);
}
Widget _buildDataCard(DashboardStatsEntity stats) {
final value = valueExtractor(stats);
final subtitle = subtitleExtractor?.call(stats);
final color = customColor ?? DashboardTheme.royalBlue;
return GestureDetector(
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),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Container(
height: 16,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
Container(
height: 32,
width: 80,
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),
),
),
],
),
);
}
Widget _buildErrorCard(String message) {
return 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: 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,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,420 @@
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';
/// Widget des événements à venir connecté au backend
class ConnectedUpcomingEvents extends StatelessWidget {
final int maxItems;
final VoidCallback? onSeeAll;
const ConnectedUpcomingEvents({
super.key,
this.maxItems = 3,
this.onSeeAll,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, 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);
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
return _buildEmptyState();
},
),
],
),
);
}
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 SizedBox(width: DashboardTheme.spacing12),
const Expanded(
child: Text(
'Événements à venir',
style: DashboardTheme.titleMedium,
),
),
if (onSeeAll != null)
TextButton(
onPressed: onSeeAll,
child: Text(
'Voir tout',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildEventsList(List<UpcomingEventEntity> events) {
if (events.isEmpty) {
return _buildEmptyState();
}
final displayEvents = events.take(maxItems).toList();
return Column(
children: displayEvents.asMap().entries.map((entry) {
final index = entry.key;
final event = entry.value;
final isLast = index == displayEvents.length - 1;
return Column(
children: [
_buildEventCard(event),
if (!isLast) const SizedBox(height: DashboardTheme.spacing12),
],
);
}).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),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
// Image ou icône
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: event.imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
child: Image.network(
event.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.event,
color: DashboardTheme.royalBlue,
size: 24,
),
),
)
: const Icon(
Icons.event,
color: DashboardTheme.royalBlue,
size: 24,
),
),
const SizedBox(width: DashboardTheme.spacing12),
// Contenu principal
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: DashboardTheme.titleSmall,
maxLines: 2,
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),
Expanded(
child: Text(
event.location,
style: DashboardTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
// Badge de temps
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
),
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),
),
child: Text(
event.daysUntilEvent,
style: DashboardTheme.bodySmall.copyWith(
color: event.isToday
? DashboardTheme.success
: event.isTomorrow
? DashboardTheme.warning
: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: DashboardTheme.spacing12),
// Barre de progression des participants
Row(
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,
),
),
],
),
),
],
),
// 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(),
),
],
],
),
);
}
Widget _buildLoadingList() {
return Column(
children: List.generate(2, (index) => Column(
children: [
_buildLoadingCard(),
if (index < 1) const SizedBox(height: DashboardTheme.spacing12),
],
)),
);
}
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),
),
),
],
),
);
}
Widget _buildErrorState(String message) {
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,
),
],
),
);
}
Widget _buildEmptyState() {
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,
),
],
),
);
}
}

View File

@@ -0,0 +1,191 @@
/// Widget de menu latéral (drawer) du dashboard
/// Navigation principale de l'application
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';
/// 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;
/// Constructeur du modèle d'élément de menu
const DrawerMenuItem({
required this.icon,
required this.title,
this.onTap,
});
}
/// Widget de menu latéral
///
/// Affiche la navigation principale avec :
/// - Header avec profil utilisateur
/// - Menu de navigation structuré
/// - Actions secondaires
/// - Design Material avec gradient
class DashboardDrawer extends StatelessWidget {
/// Callback pour les actions de navigation
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,
children: [
_buildDrawerHeader(),
...mainItems.map((item) => _buildMenuItem(item)),
const Divider(),
...secondaryItems.map((item) => _buildMenuItem(item)),
const Divider(),
_buildLogoutItem(),
],
),
);
}
/// 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,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
size: 35,
color: ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.md),
Text(
'Utilisateur UnionFlow',
style: TypographyTokens.titleMedium.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
Text(
'admin@unionflow.dev',
style: TypographyTokens.bodySmall.copyWith(
color: Colors.white.withOpacity(0.8),
),
),
],
),
);
}
/// 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

@@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
/// Widget de statistique simple pour les dashboards de rôle
class DashboardStat extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color? color;
const DashboardStat({
super.key,
required this.title,
required this.value,
required this.icon,
this.color,
});
@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,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 24,
),
const Spacer(),
Text(
value,
style: DashboardTheme.titleLarge.copyWith(
color: color ?? DashboardTheme.royalBlue,
),
),
],
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
title,
style: DashboardTheme.bodyMedium,
),
],
),
);
}
}
/// Widget de grille de statistiques
class DashboardStatsGrid extends StatelessWidget {
final List<DashboardStat> stats;
final Function(String)? onStatTap;
const DashboardStatsGrid({
super.key,
required this.stats,
this.onStatTap,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.2,
children: stats,
);
}
}
/// Widget de grille d'actions rapides
class DashboardQuickActionsGrid extends StatelessWidget {
final List<Widget> children;
const DashboardQuickActionsGrid({
super.key,
required this.children,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.5,
children: children,
);
}
}
/// Widget d'action rapide
class DashboardQuickAction extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onTap;
final Color? color;
const DashboardQuickAction({
super.key,
required this.title,
required this.icon,
required this.onTap,
this.color,
});
@override
Widget build(BuildContext context) {
return InkWell(
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(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
/// Widget de section d'activités récentes
class DashboardRecentActivitySection extends StatelessWidget {
final List<Widget> children;
const DashboardRecentActivitySection({
super.key,
required this.children,
});
@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,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Activités récentes',
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing16),
...children,
],
),
);
}
}
/// Widget d'activité
class DashboardActivity extends StatelessWidget {
final String title;
final String subtitle;
final String time;
final IconData icon;
final Color? color;
const DashboardActivity({
super.key,
required this.title,
required this.subtitle,
required this.time,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: DashboardTheme.spacing12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: (color ?? DashboardTheme.royalBlue).withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Icon(
icon,
color: color ?? DashboardTheme.royalBlue,
size: 16,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
subtitle,
style: DashboardTheme.bodySmall,
),
],
),
),
Text(
time,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey500,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,439 @@
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';
/// Widget de métriques en temps réel avec animations
class RealTimeMetricsWidget extends StatefulWidget {
final String organizationId;
final String userId;
final Duration refreshInterval;
const RealTimeMetricsWidget({
super.key,
required this.organizationId,
required this.userId,
this.refreshInterval = const Duration(minutes: 5),
});
@override
State<RealTimeMetricsWidget> createState() => _RealTimeMetricsWidgetState();
}
class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
with TickerProviderStateMixin {
Timer? _refreshTimer;
late AnimationController _pulseController;
late AnimationController _countController;
late Animation<double> _pulseAnimation;
late Animation<double> _countAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
_startAutoRefresh();
}
void _setupAnimations() {
_pulseController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_countController = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_pulseAnimation = Tween<double>(
begin: 1.0,
end: 1.1,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
_countAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _countController,
curve: Curves.easeOutCubic,
));
_pulseController.repeat(reverse: true);
}
void _startAutoRefresh() {
_refreshTimer = Timer.periodic(widget.refreshInterval, (timer) {
if (mounted) {
context.read<DashboardBloc>().add(RefreshDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
}
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.gradientCardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing20),
BlocConsumer<DashboardBloc, DashboardState>(
listener: (context, state) {
if (state is DashboardLoaded) {
_countController.forward(from: 0);
}
},
builder: (context, state) {
if (state is DashboardLoading) {
return _buildLoadingMetrics();
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildMetrics(data);
} else if (state is DashboardError) {
return _buildErrorMetrics();
}
return _buildEmptyMetrics();
},
),
],
),
);
}
Widget _buildHeader() {
return Row(
children: [
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _pulseAnimation.value,
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.speed,
color: DashboardTheme.white,
size: 24,
),
),
);
},
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Métriques Temps Réel',
style: DashboardTheme.titleMedium.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
'Mise à jour automatique',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
),
),
],
),
),
_buildRefreshIndicator(),
],
);
}
Widget _buildRefreshIndicator() {
return BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardRefreshing) {
return const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
),
);
}
return GestureDetector(
onTap: () {
context.read<DashboardBloc>().add(RefreshDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing4),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.refresh,
color: DashboardTheme.white,
size: 16,
),
),
);
},
);
}
Widget _buildMetrics(DashboardEntity data) {
return AnimatedBuilder(
animation: _countAnimation,
builder: (context, child) {
return Column(
children: [
Row(
children: [
Expanded(
child: _buildMetricItem(
'Membres Actifs',
(data.stats.activeMembers * _countAnimation.value).round(),
data.stats.totalMembers,
Icons.people,
DashboardTheme.success,
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: _buildMetricItem(
'Engagement',
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
100,
Icons.favorite,
DashboardTheme.warning,
suffix: '%',
),
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
Row(
children: [
Expanded(
child: _buildMetricItem(
'Événements',
(data.stats.upcomingEvents * _countAnimation.value).round(),
data.stats.totalEvents,
Icons.event,
DashboardTheme.info,
),
),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(
child: _buildMetricItem(
'Croissance',
(data.stats.monthlyGrowth * _countAnimation.value),
null,
Icons.trending_up,
data.stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
suffix: '%',
isDecimal: true,
),
),
],
),
],
);
},
);
}
Widget _buildMetricItem(
String label,
dynamic value,
int? maxValue,
IconData icon,
Color color, {
String suffix = '',
bool isDecimal = false,
}) {
String displayValue;
if (isDecimal) {
displayValue = value.toStringAsFixed(1) + suffix;
} else {
displayValue = value.toString() + suffix;
}
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(
color: DashboardTheme.white.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: color,
size: 20,
),
const SizedBox(width: DashboardTheme.spacing8),
Expanded(
child: Text(
label,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.8),
),
),
),
],
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
displayValue,
style: DashboardTheme.titleLarge.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
if (maxValue != null) ...[
const SizedBox(height: DashboardTheme.spacing4),
Text(
'sur $maxValue',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white.withOpacity(0.6),
),
),
],
],
),
);
}
Widget _buildLoadingMetrics() {
return Column(
children: [
Row(
children: [
Expanded(child: _buildLoadingMetricItem()),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(child: _buildLoadingMetricItem()),
],
),
const SizedBox(height: DashboardTheme.spacing16),
Row(
children: [
Expanded(child: _buildLoadingMetricItem()),
const SizedBox(width: DashboardTheme.spacing16),
Expanded(child: _buildLoadingMetricItem()),
],
),
],
);
}
Widget _buildLoadingMetricItem() {
return Container(
height: 100,
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
),
),
);
}
Widget _buildErrorMetrics() {
return Container(
height: 200,
decoration: BoxDecoration(
color: DashboardTheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
),
),
],
),
),
);
}
Widget _buildEmptyMetrics() {
return Container(
height: 200,
decoration: BoxDecoration(
color: DashboardTheme.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.speed,
color: DashboardTheme.white.withOpacity(0.5),
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Aucune donnée',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.white.withOpacity(0.7),
),
),
],
),
),
);
}
@override
void dispose() {
_refreshTimer?.cancel();
_pulseController.dispose();
_countController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,509 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../data/services/dashboard_performance_monitor.dart';
/// Widget de monitoring des performances en temps réel
class PerformanceMonitorWidget extends StatefulWidget {
final bool showDetails;
final Duration updateInterval;
const PerformanceMonitorWidget({
super.key,
this.showDetails = false,
this.updateInterval = const Duration(seconds: 2),
});
@override
State<PerformanceMonitorWidget> createState() => _PerformanceMonitorWidgetState();
}
class _PerformanceMonitorWidgetState extends State<PerformanceMonitorWidget>
with TickerProviderStateMixin {
final DashboardPerformanceMonitor _monitor = DashboardPerformanceMonitor();
StreamSubscription<PerformanceMetrics>? _metricsSubscription;
StreamSubscription<PerformanceAlert>? _alertSubscription;
PerformanceMetrics? _currentMetrics;
final List<PerformanceAlert> _recentAlerts = [];
late AnimationController _pulseController;
late Animation<double> _pulseAnimation;
bool _isExpanded = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startMonitoring();
}
void _setupAnimations() {
_pulseController = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_pulseAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _pulseController,
curve: Curves.easeInOut,
));
_pulseController.repeat(reverse: true);
}
Future<void> _startMonitoring() async {
await _monitor.startMonitoring();
_metricsSubscription = _monitor.metricsStream.listen((metrics) {
if (mounted) {
setState(() {
_currentMetrics = metrics;
});
}
});
_alertSubscription = _monitor.alertStream.listen((alert) {
if (mounted) {
setState(() {
_recentAlerts.insert(0, alert);
if (_recentAlerts.length > 5) {
_recentAlerts.removeLast();
}
});
// Afficher une notification pour les alertes critiques
if (alert.severity == AlertSeverity.error ||
alert.severity == AlertSeverity.critical) {
_showAlertSnackBar(alert);
}
}
});
}
void _showAlertSnackBar(PerformanceAlert alert) {
final color = _getAlertColor(alert.severity);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
_getAlertIcon(alert.type),
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
alert.message,
style: const TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: color,
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'Détails',
textColor: Colors.white,
onPressed: () {
setState(() {
_isExpanded = true;
});
},
),
),
);
}
@override
Widget build(BuildContext context) {
if (_currentMetrics == null) {
return _buildLoadingWidget();
}
return Container(
margin: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.subtleShadow,
),
child: Column(
children: [
_buildHeader(),
if (_isExpanded || widget.showDetails) ...[
const Divider(height: 1),
_buildDetailedMetrics(),
if (_recentAlerts.isNotEmpty) ...[
const Divider(height: 1),
_buildAlertsSection(),
],
],
],
),
);
}
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(
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.royalBlue),
),
),
SizedBox(width: DashboardTheme.spacing12),
Text(
'Initialisation du monitoring...',
style: DashboardTheme.bodyMedium,
),
],
),
);
}
Widget _buildHeader() {
return InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
child: Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Row(
children: [
AnimatedBuilder(
animation: _pulseAnimation,
builder: (context, child) {
return Transform.scale(
scale: _pulseAnimation.value,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getOverallHealthColor(),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: _getOverallHealthColor().withOpacity(0.5),
blurRadius: 4,
spreadRadius: 1,
),
],
),
),
);
},
),
const SizedBox(width: DashboardTheme.spacing12),
const Expanded(
child: Text(
'Performances Système',
style: DashboardTheme.titleSmall,
),
),
_buildQuickMetrics(),
const SizedBox(width: DashboardTheme.spacing8),
Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
color: DashboardTheme.grey600,
),
],
),
),
);
}
Widget _buildQuickMetrics() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildQuickMetric(
'MEM',
'${_currentMetrics!.memoryUsage.toStringAsFixed(0)}MB',
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
),
const SizedBox(width: DashboardTheme.spacing8),
_buildQuickMetric(
'CPU',
'${_currentMetrics!.cpuUsage.toStringAsFixed(0)}%',
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
),
const SizedBox(width: DashboardTheme.spacing8),
_buildQuickMetric(
'NET',
'${_currentMetrics!.networkLatency}ms',
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
),
],
);
}
Widget _buildQuickMetric(String label, String value, Color color) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: const TextStyle(
fontSize: 10,
color: DashboardTheme.grey600,
fontWeight: FontWeight.w500,
),
),
Text(
value,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildDetailedMetrics() {
return Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
children: [
_buildMetricRow(
'Mémoire',
'${_currentMetrics!.memoryUsage.toStringAsFixed(1)} MB',
_currentMetrics!.memoryUsage / 1000, // Normaliser sur 1000MB
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
Icons.memory,
),
const SizedBox(height: DashboardTheme.spacing12),
_buildMetricRow(
'Processeur',
'${_currentMetrics!.cpuUsage.toStringAsFixed(1)}%',
_currentMetrics!.cpuUsage / 100,
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
Icons.speed,
),
const SizedBox(height: DashboardTheme.spacing12),
_buildMetricRow(
'Réseau',
'${_currentMetrics!.networkLatency} ms',
(_currentMetrics!.networkLatency / 2000).clamp(0.0, 1.0),
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
Icons.wifi,
),
const SizedBox(height: DashboardTheme.spacing12),
_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,
),
const SizedBox(height: DashboardTheme.spacing12),
_buildMetricRow(
'Batterie',
'${_currentMetrics!.batteryLevel.toStringAsFixed(0)}%',
_currentMetrics!.batteryLevel / 100,
_getBatteryColor(_currentMetrics!.batteryLevel),
Icons.battery_std,
),
],
),
);
}
Widget _buildMetricRow(
String label,
String value,
double progress,
Color color,
IconData icon,
) {
return Row(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: DashboardTheme.spacing8),
Expanded(
flex: 2,
child: Text(
label,
style: DashboardTheme.bodySmall,
),
),
Expanded(
flex: 3,
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: DashboardTheme.grey200,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
),
const SizedBox(width: DashboardTheme.spacing8),
SizedBox(
width: 60,
child: Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
textAlign: TextAlign.end,
),
),
],
);
}
Widget _buildAlertsSection() {
return Padding(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Alertes Récentes',
style: DashboardTheme.titleSmall,
),
const SizedBox(height: DashboardTheme.spacing8),
..._recentAlerts.take(3).map((alert) => _buildAlertItem(alert)),
],
),
);
}
Widget _buildAlertItem(PerformanceAlert alert) {
final color = _getAlertColor(alert.severity);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(
_getAlertIcon(alert.type),
size: 16,
color: color,
),
const SizedBox(width: DashboardTheme.spacing8),
Expanded(
child: Text(
alert.message,
style: const TextStyle(
fontSize: 12,
color: DashboardTheme.grey700,
),
),
),
Text(
_formatTime(alert.timestamp),
style: const TextStyle(
fontSize: 10,
color: DashboardTheme.grey500,
),
),
],
),
);
}
Color _getOverallHealthColor() {
if (_currentMetrics == null) return DashboardTheme.grey400;
final metrics = _currentMetrics!;
// Calculer un score de santé global
int issues = 0;
if (metrics.memoryUsage > 500) issues++;
if (metrics.cpuUsage > 70) issues++;
if (metrics.networkLatency > 1000) issues++;
if (metrics.frameRate < 30) issues++;
switch (issues) {
case 0:
return DashboardTheme.success;
case 1:
return DashboardTheme.warning;
default:
return DashboardTheme.error;
}
}
Color _getMetricColor(double value, double warningThreshold, double errorThreshold) {
if (value >= errorThreshold) return DashboardTheme.error;
if (value >= warningThreshold) return DashboardTheme.warning;
return DashboardTheme.success;
}
Color _getBatteryColor(double batteryLevel) {
if (batteryLevel <= 20) return DashboardTheme.error;
if (batteryLevel <= 50) return DashboardTheme.warning;
return DashboardTheme.success;
}
Color _getAlertColor(AlertSeverity severity) {
switch (severity) {
case AlertSeverity.info:
return DashboardTheme.info;
case AlertSeverity.warning:
return DashboardTheme.warning;
case AlertSeverity.error:
return DashboardTheme.error;
case AlertSeverity.critical:
return DashboardTheme.error;
}
}
IconData _getAlertIcon(AlertType type) {
switch (type) {
case AlertType.memory:
return Icons.memory;
case AlertType.cpu:
return Icons.speed;
case AlertType.network:
return Icons.wifi_off;
case AlertType.performance:
return Icons.slow_motion_video;
case AlertType.battery:
return Icons.battery_alert;
case AlertType.disk:
return Icons.storage;
}
}
String _formatTime(DateTime time) {
final now = DateTime.now();
final diff = now.difference(time);
if (diff.inMinutes < 1) return 'maintenant';
if (diff.inMinutes < 60) return '${diff.inMinutes}min';
if (diff.inHours < 24) return '${diff.inHours}h';
return '${diff.inDays}j';
}
@override
void dispose() {
_pulseController.dispose();
_metricsSubscription?.cancel();
_alertSubscription?.cancel();
_monitor.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../pages/connected_dashboard_page.dart';
import '../../pages/advanced_dashboard_page.dart';
/// Widget de navigation pour les différents types de dashboard
class DashboardNavigation extends StatefulWidget {
final String organizationId;
final String userId;
const DashboardNavigation({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<DashboardNavigation> createState() => _DashboardNavigationState();
}
class _DashboardNavigationState extends State<DashboardNavigation> {
int _currentIndex = 0;
final List<DashboardTab> _tabs = [
const DashboardTab(
title: 'Accueil',
icon: Icons.home,
activeIcon: Icons.home,
type: DashboardType.home,
),
const DashboardTab(
title: 'Analytics',
icon: Icons.analytics_outlined,
activeIcon: Icons.analytics,
type: DashboardType.analytics,
),
const DashboardTab(
title: 'Rapports',
icon: Icons.assessment_outlined,
activeIcon: Icons.assessment,
type: DashboardType.reports,
),
const DashboardTab(
title: 'Paramètres',
icon: Icons.settings_outlined,
activeIcon: Icons.settings,
type: DashboardType.settings,
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildCurrentPage(),
bottomNavigationBar: _buildBottomNavigationBar(),
floatingActionButton: _buildFloatingActionButton(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
Widget _buildCurrentPage() {
switch (_tabs[_currentIndex].type) {
case DashboardType.home:
return ConnectedDashboardPage(
organizationId: widget.organizationId,
userId: widget.userId,
);
case DashboardType.analytics:
return AdvancedDashboardPage(
organizationId: widget.organizationId,
userId: widget.userId,
);
case DashboardType.reports:
return _buildReportsPage();
case DashboardType.settings:
return _buildSettingsPage();
}
}
Widget _buildBottomNavigationBar() {
return Container(
decoration: BoxDecoration(
color: DashboardTheme.white,
boxShadow: [
BoxShadow(
color: DashboardTheme.grey900.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 8,
color: DashboardTheme.white,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: DashboardTheme.spacing8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isActive = index == _currentIndex;
// Skip the middle item for FAB space
if (index == 2) {
return const SizedBox(width: 40);
}
return _buildNavItem(tab, isActive, index);
}).toList(),
),
),
),
);
}
Widget _buildNavItem(DashboardTab tab, bool isActive, int index) {
return GestureDetector(
onTap: () => setState(() => _currentIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: DashboardTheme.spacing12,
horizontal: DashboardTheme.spacing16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isActive ? tab.activeIcon : tab.icon,
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
tab.title,
style: DashboardTheme.bodySmall.copyWith(
color: isActive ? DashboardTheme.royalBlue : DashboardTheme.grey400,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
),
),
],
),
),
);
}
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,
),
),
);
}
Widget _buildReportsPage() {
return Scaffold(
appBar: AppBar(
title: const Text('Rapports'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.assessment,
size: 64,
color: DashboardTheme.grey400,
),
const SizedBox(height: DashboardTheme.spacing16),
const Text(
'Page Rapports',
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Fonctionnalité en cours de développement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
),
],
),
),
);
}
Widget _buildSettingsPage() {
return Scaffold(
appBar: AppBar(
title: const Text('Paramètres'),
backgroundColor: DashboardTheme.royalBlue,
foregroundColor: DashboardTheme.white,
automaticallyImplyLeading: false,
),
body: ListView(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
children: [
_buildSettingsSection(
'Apparence',
[
_buildSettingsTile(
'Thème',
'Bleu Roi & Pétrole',
Icons.palette,
() {},
),
_buildSettingsTile(
'Langue',
'Français',
Icons.language,
() {},
),
],
),
const SizedBox(height: DashboardTheme.spacing24),
_buildSettingsSection(
'Notifications',
[
_buildSettingsTile(
'Notifications push',
'Activées',
Icons.notifications,
() {},
),
_buildSettingsTile(
'Emails',
'Quotidien',
Icons.email,
() {},
),
],
),
const SizedBox(height: DashboardTheme.spacing24),
_buildSettingsSection(
'Données',
[
_buildSettingsTile(
'Synchronisation',
'Automatique',
Icons.sync,
() {},
),
_buildSettingsTile(
'Cache',
'Vider le cache',
Icons.storage,
() {},
),
],
),
],
),
);
}
Widget _buildSettingsSection(String title, List<Widget> children) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing12),
Container(
decoration: DashboardTheme.cardDecoration,
child: Column(children: children),
),
],
);
}
Widget _buildSettingsTile(
String title,
String subtitle,
IconData icon,
VoidCallback onTap,
) {
return ListTile(
leading: Icon(icon, color: DashboardTheme.royalBlue),
title: Text(title, style: DashboardTheme.bodyMedium),
subtitle: Text(subtitle, style: DashboardTheme.bodySmall),
trailing: const Icon(
Icons.chevron_right,
color: DashboardTheme.grey400,
),
onTap: onTap,
);
}
void _showQuickActions() {
showModalBottomSheet(
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),
),
),
padding: const EdgeInsets.all(DashboardTheme.spacing20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: DashboardTheme.grey300,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: DashboardTheme.spacing20),
const Text(
'Actions Rapides',
style: DashboardTheme.titleMedium,
),
const SizedBox(height: DashboardTheme.spacing20),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: DashboardTheme.spacing16,
mainAxisSpacing: DashboardTheme.spacing16,
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),
],
),
const SizedBox(height: DashboardTheme.spacing20),
],
),
),
);
}
Widget _buildQuickActionItem(String title, IconData icon, Color color) {
return GestureDetector(
onTap: () {
Navigator.pop(context);
// Action rapide non encore connectée
},
child: Container(
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(color: color.withOpacity(0.3)),
),
padding: const EdgeInsets.all(DashboardTheme.spacing12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: DashboardTheme.spacing8),
Text(
title,
style: DashboardTheme.bodySmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class DashboardTab {
final String title;
final IconData icon;
final IconData activeIcon;
final DashboardType type;
const DashboardTab({
required this.title,
required this.icon,
required this.activeIcon,
required this.type,
});
}
enum DashboardType {
home,
analytics,
reports,
settings,
}

View File

@@ -0,0 +1,443 @@
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';
/// Widget de notifications pour le dashboard
class DashboardNotificationsWidget extends StatelessWidget {
final int maxNotifications;
const DashboardNotificationsWidget({
super.key,
this.maxNotifications = 5,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoading) {
return _buildLoadingNotifications();
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildNotifications(data);
} else if (state is DashboardError) {
return _buildErrorNotifications();
}
return _buildEmptyNotifications();
},
),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(DashboardTheme.borderRadius),
topRight: Radius.circular(DashboardTheme.borderRadius),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: const Icon(
Icons.notifications,
color: DashboardTheme.white,
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Text(
'Notifications',
style: DashboardTheme.titleMedium.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.bold,
),
),
),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
final urgentCount = _getUrgentNotificationsCount(data);
if (urgentCount > 0) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
),
decoration: BoxDecoration(
color: DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Text(
urgentCount.toString(),
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
),
),
);
}
}
return const SizedBox.shrink();
},
),
],
),
);
}
Widget _buildNotifications(DashboardEntity data) {
final notifications = _generateNotifications(data);
if (notifications.isEmpty) {
return _buildEmptyNotifications();
}
return Column(
children: notifications.take(maxNotifications).map((notification) {
return _buildNotificationItem(notification);
}).toList(),
);
}
Widget _buildNotificationItem(DashboardNotification notification) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
width: 1,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: notification.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Icon(
notification.icon,
color: notification.color,
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
notification.title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
),
if (notification.isUrgent) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing6,
vertical: DashboardTheme.spacing2,
),
decoration: BoxDecoration(
color: DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Text(
'URGENT',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
],
],
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
notification.message,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey600,
),
),
const SizedBox(height: DashboardTheme.spacing8),
Row(
children: [
Text(
notification.timeAgo,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey500,
fontSize: 11,
),
),
const Spacer(),
if (notification.actionLabel != null) ...[
GestureDetector(
onTap: notification.onAction,
child: Text(
notification.actionLabel!,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
),
),
),
],
],
),
],
),
),
],
),
);
}
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),
),
),
],
),
),
],
),
);
}),
);
}
Widget _buildErrorNotifications() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing24),
child: Center(
child: Column(
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
),
),
],
),
),
);
}
Widget _buildEmptyNotifications() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing24),
child: Center(
child: Column(
children: [
const Icon(
Icons.notifications_none,
color: DashboardTheme.grey400,
size: 32,
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
'Aucune notification',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
'Vous êtes à jour !',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey400,
),
),
],
),
),
);
}
List<DashboardNotification> _generateNotifications(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,
timeAgo: '2h',
isUrgent: data.stats.pendingRequests > 20,
actionLabel: 'Voir',
onAction: () {},
));
}
// Notification pour les événements aujourd'hui
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,
timeAgo: '30min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () {},
));
}
// Notification pour la croissance
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,
timeAgo: '1j',
isUrgent: false,
actionLabel: null,
onAction: null,
));
}
// 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,
timeAgo: '3h',
isUrgent: data.stats.engagementRate < 0.5,
actionLabel: 'Améliorer',
onAction: () {},
));
}
// Notification pour les nouveaux membres
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,
timeAgo: '15min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () {},
));
}
return notifications;
}
int _getUrgentNotificationsCount(DashboardEntity data) {
final notifications = _generateNotifications(data);
return notifications.where((n) => n.isUrgent).length;
}
}
class DashboardNotification {
final String title;
final String message;
final IconData icon;
final Color color;
final String timeAgo;
final bool isUrgent;
final String? actionLabel;
final VoidCallback? onAction;
const DashboardNotification({
required this.title,
required this.message,
required this.icon,
required this.color,
required this.timeAgo,
required this.isUrgent,
this.actionLabel,
this.onAction,
});
}

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
/// Widget de recherche rapide pour le dashboard
class DashboardSearchWidget extends StatefulWidget {
final Function(String)? onSearch;
final String? hintText;
final List<SearchSuggestion>? suggestions;
const DashboardSearchWidget({
super.key,
this.onSearch,
this.hintText,
this.suggestions,
});
@override
State<DashboardSearchWidget> createState() => _DashboardSearchWidgetState();
}
class _DashboardSearchWidgetState extends State<DashboardSearchWidget>
with TickerProviderStateMixin {
final TextEditingController _searchController = TextEditingController();
final FocusNode _focusNode = FocusNode();
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
bool _isExpanded = false;
List<SearchSuggestion> _filteredSuggestions = [];
@override
void initState() {
super.initState();
_setupAnimations();
_setupListeners();
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 1.05,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
void _setupListeners() {
_focusNode.addListener(() {
setState(() {
_isExpanded = _focusNode.hasFocus;
});
if (_focusNode.hasFocus) {
_animationController.forward();
} else {
_animationController.reverse();
}
});
_searchController.addListener(() {
_filterSuggestions(_searchController.text);
});
}
void _filterSuggestions(String query) {
if (query.isEmpty) {
setState(() {
_filteredSuggestions = widget.suggestions ?? _getDefaultSuggestions();
});
return;
}
final filtered = (widget.suggestions ?? _getDefaultSuggestions())
.where((suggestion) =>
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
.toList();
setState(() {
_filteredSuggestions = filtered;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildSearchBar(),
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
const SizedBox(height: DashboardTheme.spacing8),
_buildSuggestions(),
],
],
);
}
Widget _buildSearchBar() {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
boxShadow: _isExpanded ? DashboardTheme.elevatedShadow : DashboardTheme.subtleShadow,
),
child: TextField(
controller: _searchController,
focusNode: _focusNode,
onSubmitted: (value) {
if (value.isNotEmpty) {
widget.onSearch?.call(value);
_focusNode.unfocus();
}
},
decoration: InputDecoration(
hintText: widget.hintText ?? 'Rechercher...',
hintStyle: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey400,
),
prefixIcon: Icon(
Icons.search,
color: _isExpanded ? DashboardTheme.royalBlue : DashboardTheme.grey400,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
_focusNode.unfocus();
},
icon: const Icon(
Icons.clear,
color: DashboardTheme.grey400,
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusLarge),
borderSide: const BorderSide(
color: DashboardTheme.royalBlue,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing16,
vertical: DashboardTheme.spacing12,
),
filled: true,
fillColor: DashboardTheme.white,
),
style: DashboardTheme.bodyMedium,
),
),
);
},
);
}
Widget _buildSuggestions() {
return Container(
constraints: const BoxConstraints(maxHeight: 300),
decoration: BoxDecoration(
color: DashboardTheme.white,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
boxShadow: DashboardTheme.elevatedShadow,
),
child: ListView.builder(
shrinkWrap: true,
itemCount: _filteredSuggestions.length,
itemBuilder: (context, index) {
final suggestion = _filteredSuggestions[index];
return _buildSuggestionItem(suggestion, index == _filteredSuggestions.length - 1);
},
),
);
}
Widget _buildSuggestionItem(SearchSuggestion suggestion, bool isLast) {
return InkWell(
onTap: () {
_searchController.text = suggestion.title;
widget.onSearch?.call(suggestion.title);
_focusNode.unfocus();
suggestion.onTap?.call();
},
child: Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
border: isLast
? null
: const Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
width: 1,
),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: suggestion.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Icon(
suggestion.icon,
color: suggestion.color,
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
suggestion.title,
style: DashboardTheme.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
if (suggestion.subtitle.isNotEmpty) ...[
const SizedBox(height: DashboardTheme.spacing2),
Text(
suggestion.subtitle,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey600,
),
),
],
],
),
),
const Icon(
Icons.arrow_forward_ios,
color: DashboardTheme.grey400,
size: 16,
),
],
),
),
);
}
List<SearchSuggestion> _getDefaultSuggestions() {
return [
SearchSuggestion(
title: 'Membres',
subtitle: 'Rechercher des membres',
icon: Icons.people,
color: DashboardTheme.royalBlue,
onTap: () {},
),
SearchSuggestion(
title: 'Événements',
subtitle: 'Trouver des événements',
icon: Icons.event,
color: DashboardTheme.tealBlue,
onTap: () {},
),
SearchSuggestion(
title: 'Contributions',
subtitle: 'Historique des paiements',
icon: Icons.payment,
color: DashboardTheme.success,
onTap: () {},
),
SearchSuggestion(
title: 'Rapports',
subtitle: 'Consulter les rapports',
icon: Icons.assessment,
color: DashboardTheme.warning,
onTap: () {},
),
SearchSuggestion(
title: 'Paramètres',
subtitle: 'Configuration système',
icon: Icons.settings,
color: DashboardTheme.grey600,
onTap: () {},
),
];
}
@override
void dispose() {
_searchController.dispose();
_focusNode.dispose();
_animationController.dispose();
super.dispose();
}
}
class SearchSuggestion {
final String title;
final String subtitle;
final IconData icon;
final Color color;
final VoidCallback? onTap;
const SearchSuggestion({
required this.title,
required this.subtitle,
required this.icon,
required this.color,
this.onTap,
});
}

View File

@@ -0,0 +1,337 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme_manager.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
/// Widget de sélection de thème pour le Dashboard
class ThemeSelectorWidget extends StatefulWidget {
final Function(String)? onThemeChanged;
const ThemeSelectorWidget({
super.key,
this.onThemeChanged,
});
@override
State<ThemeSelectorWidget> createState() => _ThemeSelectorWidgetState();
}
class _ThemeSelectorWidgetState extends State<ThemeSelectorWidget> {
String _selectedTheme = 'royalTeal';
@override
void initState() {
super.initState();
_selectedTheme = DashboardThemeManager.currentTheme.name == 'Bleu Roi & Pétrole'
? 'royalTeal' : 'royalTeal'; // Par défaut
}
@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,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.palette,
color: DashboardTheme.royalBlue,
size: 24,
),
SizedBox(width: DashboardTheme.spacing8),
Text(
'Thème de l\'interface',
style: DashboardTheme.titleMedium,
),
],
),
const SizedBox(height: DashboardTheme.spacing16),
// Grille des thèmes
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: DashboardTheme.spacing12,
mainAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.5,
),
itemCount: DashboardThemeManager.availableThemes.length,
itemBuilder: (context, index) {
final themeOption = DashboardThemeManager.availableThemes[index];
final isSelected = _selectedTheme == themeOption.key;
return _buildThemeCard(themeOption, isSelected);
},
),
const SizedBox(height: DashboardTheme.spacing16),
// Aperçu du thème sélectionné
_buildThemePreview(),
],
),
);
}
Widget _buildThemeCard(ThemeOption themeOption, bool isSelected) {
return GestureDetector(
onTap: () => _selectTheme(themeOption.key),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(
color: isSelected
? themeOption.theme.primaryColor
: DashboardTheme.grey300,
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
? [
BoxShadow(
color: themeOption.theme.primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: DashboardTheme.subtleShadow,
),
child: Column(
children: [
// Gradient de démonstration
Expanded(
flex: 2,
child: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
themeOption.theme.primaryColor,
themeOption.theme.secondaryColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(DashboardTheme.borderRadius - 1),
topRight: Radius.circular(DashboardTheme.borderRadius - 1),
),
),
child: isSelected
? const Icon(
Icons.check_circle,
color: Colors.white,
size: 24,
)
: null,
),
),
// Nom du thème
Expanded(
flex: 1,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(DashboardTheme.spacing8),
decoration: BoxDecoration(
color: themeOption.theme.cardColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(DashboardTheme.borderRadius - 1),
bottomRight: Radius.circular(DashboardTheme.borderRadius - 1),
),
),
child: Center(
child: Text(
themeOption.name,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: themeOption.theme.textPrimary,
),
textAlign: TextAlign.center,
),
),
),
),
],
),
),
);
}
Widget _buildThemePreview() {
final currentTheme = DashboardThemeManager.availableThemes
.firstWhere((theme) => theme.key == _selectedTheme);
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: BoxDecoration(
color: currentTheme.theme.backgroundColor,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
border: Border.all(color: DashboardTheme.grey300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Aperçu: ${currentTheme.name}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: currentTheme.theme.textPrimary,
),
),
const SizedBox(height: DashboardTheme.spacing12),
// Exemple de carte avec le thème
Container(
width: double.infinity,
padding: const EdgeInsets.all(DashboardTheme.spacing12),
decoration: BoxDecoration(
color: currentTheme.theme.cardColor,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
boxShadow: [
BoxShadow(
color: currentTheme.theme.primaryColor.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: currentTheme.theme.primaryGradient,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.dashboard,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Dashboard UnionFlow',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: currentTheme.theme.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'Exemple avec ce thème',
style: TextStyle(
fontSize: 12,
color: currentTheme.theme.textSecondary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
),
decoration: BoxDecoration(
color: currentTheme.theme.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
child: Text(
'Actif',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: currentTheme.theme.success,
),
),
),
],
),
),
const SizedBox(height: DashboardTheme.spacing12),
// Palette de couleurs
Row(
children: [
_buildColorSwatch('Primaire', currentTheme.theme.primaryColor),
const SizedBox(width: DashboardTheme.spacing8),
_buildColorSwatch('Secondaire', currentTheme.theme.secondaryColor),
const SizedBox(width: DashboardTheme.spacing8),
_buildColorSwatch('Succès', currentTheme.theme.success),
const SizedBox(width: DashboardTheme.spacing8),
_buildColorSwatch('Attention', currentTheme.theme.warning),
],
),
],
),
);
}
Widget _buildColorSwatch(String label, Color color) {
return Expanded(
child: Column(
children: [
Container(
width: double.infinity,
height: 30,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 10,
color: DashboardTheme.grey600,
),
textAlign: TextAlign.center,
),
],
),
);
}
void _selectTheme(String themeKey) {
setState(() {
_selectedTheme = themeKey;
});
// Appliquer le thème
DashboardThemeManager.setTheme(themeKey);
// Notifier le changement
widget.onThemeChanged?.call(themeKey);
// Afficher un message de confirmation
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Thème "${DashboardThemeManager.availableThemes.firstWhere((t) => t.key == themeKey).name}" appliqué',
),
backgroundColor: DashboardThemeManager.currentTheme.success,
duration: const Duration(seconds: 2),
),
);
}
}

View File

@@ -0,0 +1,255 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.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 raccourcis rapides pour le dashboard
class DashboardShortcutsWidget extends StatelessWidget {
final List<DashboardShortcut>? customShortcuts;
final int maxShortcuts;
const DashboardShortcutsWidget({
super.key,
this.customShortcuts,
this.maxShortcuts = 6,
});
@override
Widget build(BuildContext context) {
final shortcuts = customShortcuts ?? _getDefaultShortcuts(context);
final displayShortcuts = shortcuts.take(maxShortcuts).toList();
return Container(
decoration: DashboardTheme.cardDecoration,
padding: const EdgeInsets.all(DashboardTheme.spacing20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: DashboardTheme.spacing16),
_buildShortcutsGrid(displayShortcuts),
],
),
);
}
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 SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Text(
'Actions Rapides',
style: DashboardTheme.titleMedium.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,
),
),
),
],
);
}
Widget _buildShortcutsGrid(List<DashboardShortcut> shortcuts) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: DashboardTheme.spacing12,
mainAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.0,
),
itemCount: shortcuts.length,
itemBuilder: (context, index) {
return _buildShortcutItem(shortcuts[index]);
},
);
}
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(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: 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,
),
),
),
],
],
),
),
);
}
List<DashboardShortcut> _getDefaultShortcuts(BuildContext context) {
return [
DashboardShortcut(
title: 'Nouveau\nMembre',
icon: Icons.person_add,
color: DashboardTheme.success,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MembersPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Créer\nÉvénement',
icon: Icons.event_available,
color: DashboardTheme.royalBlue,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const EventsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Ajouter\nContribution',
icon: Icons.payment,
color: DashboardTheme.tealBlue,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ContributionsPageWrapper(),
),
);
},
),
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,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Paramètres',
icon: Icons.settings,
color: DashboardTheme.grey600,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
);
},
),
];
}
}
class DashboardShortcut {
final String title;
final IconData icon;
final Color color;
final VoidCallback onTap;
final String? badge;
final Color? badgeColor;
const DashboardShortcut({
required this.title,
required this.icon,
required this.color,
required this.onTap,
this.badge,
this.badgeColor,
});
}

View File

@@ -0,0 +1,28 @@
// Export des widgets dashboard connectés
export 'connected/connected_stats_card.dart';
export 'connected/connected_recent_activities.dart';
export 'connected/connected_upcoming_events.dart';
// Export des widgets charts
export 'charts/dashboard_chart_widget.dart';
// Export des widgets metrics
export 'metrics/real_time_metrics_widget.dart';
// Export des widgets monitoring
export 'monitoring/performance_monitor_widget.dart';
// Export des widgets navigation
export 'navigation/dashboard_navigation.dart';
// Export des widgets notifications
export 'notifications/dashboard_notifications_widget.dart';
// Export des widgets search
export 'search/dashboard_search_widget.dart';
// Export des widgets settings
export 'settings/theme_selector_widget.dart';
// Export des widgets shortcuts
export 'shortcuts/dashboard_shortcuts_widget.dart';