Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 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

@@ -1,360 +0,0 @@
import 'package:flutter/material.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.
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: Colors.orange,
threshold: 80,
),
PerformanceMetric(
label: 'RAM',
value: 78.5,
unit: '%',
color: Colors.blue,
threshold: 85,
),
PerformanceMetric(
label: 'Disque',
value: 45.2,
unit: '%',
color: Colors.green,
threshold: 90,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = true;
/// Constructeur pour les métriques réseau
const PerformanceCard.network({
super.key,
this.onTap,
}) : title = 'Réseau',
subtitle = 'Trafic et latence',
metrics = const [
PerformanceMetric(
label: 'Bande passante',
value: 23.4,
unit: 'MB/s',
color: Color(0xFF6C5CE7),
threshold: 100,
),
PerformanceMetric(
label: 'Latence',
value: 12.7,
unit: 'ms',
color: Color(0xFF00B894),
threshold: 50,
),
PerformanceMetric(
label: 'Paquets perdus',
value: 0.02,
unit: '%',
color: Colors.red,
threshold: 1,
),
],
style = PerformanceCardStyle.elevated,
showValues = true,
showProgressBars = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: _getDecoration(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 12),
_buildMetrics(),
],
),
),
);
}
/// En-tête de la carte
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
);
}
/// Construction des métriques
Widget _buildMetrics() {
return Column(
children: metrics.map((metric) => Padding(
padding: const EdgeInsets.only(bottom: 8),
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 = Colors.red;
} else if (isWarning) {
effectiveColor = Colors.orange;
}
return Column(
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: effectiveColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
metric.label,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
const Spacer(),
if (showValues)
Text(
'${metric.value.toStringAsFixed(1)}${metric.unit}',
style: TextStyle(
color: effectiveColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
],
),
if (showProgressBars) ...[
const SizedBox(height: 4),
_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: Colors.grey[200],
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
),
);
}
/// Décoration selon le style
BoxDecoration _getDecoration() {
switch (style) {
case PerformanceCardStyle.elevated:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
);
case PerformanceCardStyle.outlined:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF6C5CE7).withOpacity(0.2),
width: 1,
),
);
case PerformanceCardStyle.minimal:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
);
}
}
}
/// 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;
final Map<String, dynamic>? metadata;
const PerformanceMetric({
required this.label,
required this.value,
required this.unit,
required this.color,
required this.threshold,
this.metadata,
});
/// Constructeur pour une métrique CPU
const PerformanceMetric.cpu(double value)
: label = 'CPU',
value = value,
unit = '%',
color = Colors.orange,
threshold = 80,
metadata = null;
/// Constructeur pour une métrique RAM
const PerformanceMetric.memory(double value)
: label = 'Mémoire',
value = value,
unit = '%',
color = Colors.blue,
threshold = 85,
metadata = null;
/// Constructeur pour une métrique disque
const PerformanceMetric.disk(double value)
: label = 'Disque',
value = value,
unit = '%',
color = Colors.green,
threshold = 90,
metadata = null;
/// Constructeur pour une métrique réseau
PerformanceMetric.network(double value, String unit)
: label = 'Réseau',
value = value,
unit = unit,
color = const Color(0xFF6C5CE7),
threshold = 100,
metadata = null;
/// Niveau de criticité de la métrique
MetricLevel get level {
if (value > threshold) return MetricLevel.critical;
if (value > threshold * 0.8) return MetricLevel.warning;
if (value > threshold * 0.6) return MetricLevel.normal;
return MetricLevel.good;
}
/// Couleur selon le niveau
Color get levelColor {
switch (level) {
case MetricLevel.good:
return Colors.green;
case MetricLevel.normal:
return color;
case MetricLevel.warning:
return Colors.orange;
case MetricLevel.critical:
return Colors.red;
}
}
}
/// Niveaux de métrique
enum MetricLevel {
good,
normal,
warning,
critical,
}
/// Styles de carte de performance
enum PerformanceCardStyle {
elevated,
outlined,
minimal,
}

View File

@@ -1,418 +0,0 @@
/// Dashboard Adaptatif Principal - Orchestrateur Intelligent
/// Sélectionne et affiche le dashboard approprié selon le rôle utilisateur
library adaptive_dashboard_page;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/auth_bloc.dart';
import '../../../../core/auth/models/user_role.dart';
import '../../../../core/widgets/adaptive_widget.dart';
import 'role_dashboards/super_admin_dashboard.dart';
import 'role_dashboards/org_admin_dashboard.dart';
import 'role_dashboards/moderator_dashboard.dart';
import 'role_dashboards/active_member_dashboard.dart';
import 'role_dashboards/simple_member_dashboard.dart';
import 'role_dashboards/visitor_dashboard.dart';
/// Page Dashboard Adaptatif - Le cœur du système morphique
///
/// Cette page utilise l'AdaptiveWidget pour afficher automatiquement
/// le dashboard approprié selon le rôle de l'utilisateur connecté.
///
/// Fonctionnalités :
/// - Morphing automatique entre les dashboards
/// - Animations fluides lors des changements de rôle
/// - Gestion des états de chargement et d'erreur
/// - Fallback gracieux pour les rôles non supportés
class AdaptiveDashboardPage extends StatefulWidget {
const AdaptiveDashboardPage({super.key});
@override
State<AdaptiveDashboardPage> createState() => _AdaptiveDashboardPageState();
}
class _AdaptiveDashboardPageState extends State<AdaptiveDashboardPage>
with TickerProviderStateMixin {
/// Contrôleur d'animation pour les transitions
late AnimationController _transitionController;
/// Animation de fade pour les transitions
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
}
@override
void dispose() {
_transitionController.dispose();
super.dispose();
}
/// Initialise les animations de transition
void _initializeAnimations() {
_transitionController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _transitionController,
curve: Curves.easeInOutCubic,
));
// Démarrer l'animation initiale
_transitionController.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
// Déclencher l'animation lors des changements d'état
if (state is AuthAuthenticated) {
_transitionController.reset();
_transitionController.forward();
}
},
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: _buildAdaptiveDashboard(),
);
},
),
),
);
}
/// Construit le dashboard adaptatif selon le rôle
Widget _buildAdaptiveDashboard() {
return AdaptiveWidget(
// Mapping des rôles vers leurs dashboards spécifiques
roleWidgets: {
UserRole.superAdmin: () => const SuperAdminDashboard(),
UserRole.orgAdmin: () => const OrgAdminDashboard(),
UserRole.moderator: () => const ModeratorDashboard(),
UserRole.activeMember: () => const ActiveMemberDashboard(),
UserRole.simpleMember: () => const SimpleMemberDashboard(),
UserRole.visitor: () => const VisitorDashboard(),
},
// Permissions requises pour accéder au dashboard
requiredPermissions: const [
'dashboard.view.own',
],
// Widget affiché si les permissions sont insuffisantes
fallbackWidget: _buildUnauthorizedDashboard(),
// Widget affiché pendant le chargement
loadingWidget: _buildLoadingDashboard(),
// Configuration des animations
enableMorphing: true,
morphingDuration: const Duration(milliseconds: 800),
animationCurve: Curves.easeInOutCubic,
// Audit trail activé
auditLog: true,
);
}
/// Dashboard affiché en cas d'accès non autorisé
Widget _buildUnauthorizedDashboard() {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFF8F9FA),
Color(0xFFE9ECEF),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icône d'accès refusé
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
),
child: const Icon(
Icons.lock_outline,
size: 60,
color: Colors.red,
),
),
const SizedBox(height: 32),
// Titre
Text(
'Accès Non Autorisé',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
const SizedBox(height: 16),
// Description
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Vous n\'avez pas les permissions nécessaires pour accéder au dashboard. Veuillez contacter un administrateur.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.grey[600],
),
),
),
const SizedBox(height: 32),
// Bouton de contact
ElevatedButton.icon(
onPressed: () => _onContactSupport(),
icon: const Icon(Icons.support_agent),
label: const Text('Contacter le Support'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
),
),
);
}
/// Dashboard affiché pendant le chargement
Widget _buildLoadingDashboard() {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo animé
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(seconds: 2),
builder: (context, value, child) {
return Transform.rotate(
angle: value * 2 * 3.14159,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(40),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.dashboard,
color: Colors.white,
size: 40,
),
),
);
},
),
const SizedBox(height: 32),
// Titre
Text(
'UnionFlow',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Indicateur de chargement
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
const SizedBox(height: 16),
// Message de chargement
Text(
'Préparation de votre dashboard...',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
),
);
}
/// Gère le contact avec le support
void _onContactSupport() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Contacter le Support'),
content: const Text(
'Pour obtenir de l\'aide, veuillez envoyer un email à :\n\nsupport@unionflow.com\n\nOu appelez le :\n+33 1 23 45 67 89',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Ici, on pourrait ouvrir l'app email ou téléphone
},
child: const Text('Envoyer Email'),
),
],
),
);
}
}
/// Extension pour faciliter la navigation vers le dashboard adaptatif
extension AdaptiveDashboardNavigation on BuildContext {
/// Navigue vers le dashboard adaptatif
void navigateToAdaptiveDashboard() {
Navigator.of(this).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const AdaptiveDashboardPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOutCubic,
)),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 600),
),
);
}
}
/// Mixin pour les dashboards qui ont besoin de fonctionnalités communes
mixin DashboardMixin<T extends StatefulWidget> on State<T> {
/// Affiche une notification de succès
void showSuccessNotification(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
/// Affiche une notification d'erreur
void showErrorNotification(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
/// Affiche une boîte de dialogue de confirmation
Future<bool> showConfirmationDialog(String title, String message) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirmer'),
),
],
),
);
return result ?? false;
}
}

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: () {
// TODO: Ouvrir les paramètres
},
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: () {
// TODO: Générer le rapport
},
icon: Icon(
Icons.download,
color: color,
),
),
],
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton.extended(
onPressed: () {
// TODO: Actions rapides
},
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

@@ -1,270 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Page principale du tableau de bord - Version simple
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('UnionFlow - Tableau de bord'),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notifications - Fonctionnalité à venir'),
duration: Duration(seconds: 2),
),
);
},
),
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Paramètres - Fonctionnalité à venir'),
duration: Duration(seconds: 2),
),
);
},
),
],
),
body: RefreshIndicator(
onRefresh: _refreshDashboard,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Message de bienvenue
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bienvenue sur UnionFlow',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
Text(
'Votre plateforme de gestion d\'union familiale',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
),
const SizedBox(height: 24),
// Statistiques rapides
Row(
children: [
Expanded(
child: _buildStatCard(
'Membres',
'25',
Icons.people,
Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Cotisations',
'15',
Icons.payment,
Colors.green,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
'Événements',
'8',
Icons.event,
Colors.orange,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Solidarité',
'3',
Icons.favorite,
Colors.red,
),
),
],
),
const SizedBox(height: 24),
// Actions rapides
Text(
'Actions rapides',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.5,
children: [
_buildActionCard(
'Nouveau membre',
Icons.person_add,
Colors.blue,
() => _showComingSoon('Nouveau membre'),
),
_buildActionCard(
'Nouvelle cotisation',
Icons.add_card,
Colors.green,
() => _showComingSoon('Nouvelle cotisation'),
),
_buildActionCard(
'Nouvel événement',
Icons.event_available,
Colors.orange,
() => _showComingSoon('Nouvel événement'),
),
_buildActionCard(
'Demande d\'aide',
Icons.help_outline,
Colors.red,
() => _showComingSoon('Demande d\'aide'),
),
],
),
const SizedBox(height: 24),
],
),
),
),
);
}
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: color, size: 24),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 8),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildActionCard(String title, IconData icon, Color color, VoidCallback onTap) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
void _showComingSoon(String feature) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$feature - Fonctionnalité à venir'),
duration: const Duration(seconds: 2),
),
);
}
Future<void> _refreshDashboard() async {
// Simuler un délai de rafraîchissement
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tableau de bord actualisé'),
duration: Duration(seconds: 2),
backgroundColor: Colors.green,
),
);
}
}
}

View File

@@ -1,121 +0,0 @@
/// Dashboard Page Stable - Redirecteur vers Dashboard Adaptatif
/// Redirige automatiquement vers le nouveau système de dashboard adaptatif
library dashboard_page_stable;
import 'package:flutter/material.dart';
import 'adaptive_dashboard_page.dart';
/// Page Dashboard Stable - Maintenant un redirecteur
///
/// Cette page redirige automatiquement vers le nouveau système
/// de dashboard adaptatif basé sur les rôles utilisateurs.
class DashboardPageStable extends StatefulWidget {
const DashboardPageStable({super.key});
@override
State<DashboardPageStable> createState() => _DashboardPageStableState();
}
class _DashboardPageStableState extends State<DashboardPageStable> {
@override
void initState() {
super.initState();
// Rediriger automatiquement vers le dashboard adaptatif
WidgetsBinding.instance.addPostFrameCallback((_) {
_redirectToAdaptiveDashboard();
});
}
/// Redirige vers le dashboard adaptatif
void _redirectToAdaptiveDashboard() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const AdaptiveDashboardPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOutCubic,
)),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 600),
),
);
}
@override
Widget build(BuildContext context) {
// Afficher un écran de chargement pendant la redirection
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF6C5CE7),
Color(0xFF5A4FCF),
],
),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Icon(
Icons.dashboard,
color: Colors.white,
size: 80,
),
SizedBox(height: 24),
// Titre
Text(
'UnionFlow',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16),
// Indicateur de chargement
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
SizedBox(height: 16),
// Message
Text(
'Chargement de votre dashboard...',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
),
);
}
}

View File

@@ -1,305 +0,0 @@
import 'package:flutter/material.dart';
import '../widgets/dashboard_widgets.dart';
/// Exemple de dashboard refactorisé utilisant les nouveaux composants
///
/// Ce fichier démontre comment créer un dashboard sophistiqué
/// en utilisant les composants modulaires créés lors de la refactorisation.
class ExampleRefactoredDashboard extends StatelessWidget {
const ExampleRefactoredDashboard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8F9FA),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec informations système et actions
DashboardHeader.superAdmin(
actions: [
DashboardAction(
icon: Icons.refresh,
tooltip: 'Actualiser',
onPressed: () => _handleRefresh(context),
),
DashboardAction(
icon: Icons.settings,
tooltip: 'Paramètres',
onPressed: () => _handleSettings(context),
),
],
),
const SizedBox(height: 16),
// Section des KPIs système
QuickStatsSection.systemKPIs(
onStatTap: (stat) => _handleStatTap(context, stat),
),
const SizedBox(height: 16),
// Carte de performance serveur
PerformanceCard.server(
onTap: () => _handlePerformanceTap(context),
),
const SizedBox(height: 16),
// Section des alertes récentes
RecentActivitiesSection.alerts(
onActivityTap: (activity) => _handleActivityTap(context, activity),
onViewAll: () => _handleViewAllAlerts(context),
),
const SizedBox(height: 16),
// Section des activités système
RecentActivitiesSection.system(
onActivityTap: (activity) => _handleActivityTap(context, activity),
onViewAll: () => _handleViewAllActivities(context),
),
const SizedBox(height: 16),
// Section des événements à venir
UpcomingEventsSection.systemTasks(
onEventTap: (event) => _handleEventTap(context, event),
onViewAll: () => _handleViewAllEvents(context),
),
const SizedBox(height: 16),
// Exemple de section personnalisée avec composants individuels
_buildCustomSection(context),
const SizedBox(height: 16),
// Exemple de métriques de performance réseau
PerformanceCard.network(
onTap: () => _handleNetworkTap(context),
),
],
),
),
);
}
/// Section personnalisée utilisant les composants de base
Widget _buildCustomSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader.section(
title: 'Section Personnalisée',
subtitle: 'Exemple d\'utilisation des composants de base',
icon: Icons.extension,
),
// Grille de statistiques personnalisées
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.4,
children: [
StatCard(
title: 'Connexions',
value: '1,247',
subtitle: 'Actives maintenant',
icon: Icons.wifi,
color: const Color(0xFF6C5CE7),
onTap: () => _showSnackBar(context, 'Connexions tappées'),
),
StatCard(
title: 'Erreurs',
value: '3',
subtitle: 'Dernière heure',
icon: Icons.error_outline,
color: Colors.red,
onTap: () => _showSnackBar(context, 'Erreurs tappées'),
),
StatCard(
title: 'Succès',
value: '98.7%',
subtitle: 'Taux de réussite',
icon: Icons.check_circle_outline,
color: const Color(0xFF00B894),
onTap: () => _showSnackBar(context, 'Succès tappés'),
),
StatCard(
title: 'Latence',
value: '12ms',
subtitle: 'Moyenne',
icon: Icons.speed,
color: Colors.orange,
onTap: () => _showSnackBar(context, 'Latence tappée'),
),
],
),
const SizedBox(height: 16),
// Liste d'activités personnalisées
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SectionHeader.subsection(
title: 'Activités Personnalisées',
),
ActivityItem.system(
title: 'Configuration mise à jour',
description: 'Paramètres de sécurité modifiés',
timestamp: 'il y a 10min',
onTap: () => _showSnackBar(context, 'Configuration tappée'),
),
ActivityItem.user(
title: 'Nouvel administrateur',
description: 'Jean Dupont ajouté comme admin',
timestamp: 'il y a 1h',
onTap: () => _showSnackBar(context, 'Administrateur tappé'),
),
ActivityItem.success(
title: 'Sauvegarde terminée',
description: 'Sauvegarde automatique réussie',
timestamp: 'il y a 2h',
onTap: () => _showSnackBar(context, 'Sauvegarde tappée'),
),
],
),
),
],
);
}
// Gestionnaires d'événements
void _handleRefresh(BuildContext context) {
_showSnackBar(context, 'Actualisation en cours...');
}
void _handleSettings(BuildContext context) {
_showSnackBar(context, 'Ouverture des paramètres...');
}
void _handleStatTap(BuildContext context, QuickStat stat) {
_showSnackBar(context, 'Statistique tappée: ${stat.title}');
}
void _handlePerformanceTap(BuildContext context) {
_showSnackBar(context, 'Ouverture des détails de performance...');
}
void _handleActivityTap(BuildContext context, RecentActivity activity) {
_showSnackBar(context, 'Activité tappée: ${activity.title}');
}
void _handleEventTap(BuildContext context, UpcomingEvent event) {
_showSnackBar(context, 'Événement tappé: ${event.title}');
}
void _handleViewAllAlerts(BuildContext context) {
_showSnackBar(context, 'Affichage de toutes les alertes...');
}
void _handleViewAllActivities(BuildContext context) {
_showSnackBar(context, 'Affichage de toutes les activités...');
}
void _handleViewAllEvents(BuildContext context) {
_showSnackBar(context, 'Affichage de tous les événements...');
}
void _handleNetworkTap(BuildContext context) {
_showSnackBar(context, 'Ouverture des métriques réseau...');
}
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: const Color(0xFF6C5CE7),
duration: const Duration(seconds: 2),
),
);
}
}
/// Widget de démonstration pour tester les composants
class DashboardComponentsDemo extends StatelessWidget {
const DashboardComponentsDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Démo Composants Dashboard'),
backgroundColor: const Color(0xFF6C5CE7),
foregroundColor: Colors.white,
),
body: const SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader.primary(
title: 'Démonstration des Composants',
subtitle: 'Tous les widgets refactorisés',
icon: Icons.widgets,
),
SectionHeader.section(
title: 'En-têtes de Dashboard',
),
DashboardHeader.superAdmin(),
SizedBox(height: 16),
DashboardHeader.orgAdmin(),
SizedBox(height: 16),
DashboardHeader.member(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Sections de Statistiques',
),
QuickStatsSection.systemKPIs(),
SizedBox(height: 16),
QuickStatsSection.organizationStats(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Cartes de Performance',
),
PerformanceCard.server(),
SizedBox(height: 16),
PerformanceCard.network(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Sections d\'Activités',
),
RecentActivitiesSection.system(),
SizedBox(height: 16),
RecentActivitiesSection.alerts(),
SizedBox(height: 24),
SectionHeader.section(
title: 'Événements à Venir',
),
UpcomingEventsSection.organization(),
SizedBox(height: 16),
UpcomingEventsSection.systemTasks(),
],
),
),
);
}
}

View File

@@ -3,8 +3,8 @@
library moderator_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/widgets.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Management Hub pour Modérateur
class ModeratorDashboard extends StatelessWidget {
@@ -81,34 +81,30 @@ class ModeratorDashboard extends StatelessWidget {
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
stats: [
stats: const [
DashboardStat(
icon: Icons.flag,
value: '12',
title: 'Signalements',
color: const Color(0xFFE17055),
onTap: () {},
color: Color(0xFFE17055),
),
DashboardStat(
icon: Icons.pending_actions,
value: '8',
title: 'En Attente',
color: const Color(0xFFD63031),
onTap: () {},
color: Color(0xFFD63031),
),
DashboardStat(
icon: Icons.check_circle,
value: '45',
title: 'Résolus',
color: const Color(0xFF00B894),
onTap: () {},
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.people,
value: '156',
title: 'Membres',
color: const Color(0xFF0984E3),
onTap: () {},
color: Color(0xFF0984E3),
),
],
onStatTap: (type) {},
@@ -127,37 +123,36 @@ class ModeratorDashboard extends StatelessWidget {
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
actions: [
children: [
DashboardQuickAction(
icon: Icons.gavel,
title: 'Modérer',
subtitle: 'Contenu signalé',
color: const Color(0xFFE17055),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.person_remove,
title: 'Suspendre',
subtitle: 'Membre problématique',
color: const Color(0xFFD63031),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.message,
title: 'Communiquer',
subtitle: 'Envoyer message',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.report,
title: 'Rapport',
subtitle: 'Activité modération',
color: const Color(0xFF6C5CE7),
onTap: () {},
),
],
onActionTap: (type) {},
),
],
);
@@ -213,8 +208,8 @@ class ModeratorDashboard extends StatelessWidget {
}
Widget _buildRecentActivity() {
return DashboardRecentActivitySection(
activities: const [
return const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Signalement traité',
subtitle: 'Contenu supprimé',
@@ -230,7 +225,6 @@ class ModeratorDashboard extends StatelessWidget {
time: 'Il y a 3h',
),
],
onActivityTap: (id) {},
);
}
}

View File

@@ -3,8 +3,7 @@
library org_admin_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/dashboard_widgets.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
/// Dashboard Control Panel pour Administrateur d'Organisation
@@ -236,7 +235,31 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
/// Section métriques organisation
Widget _buildOrganizationMetricsSection() {
return const QuickStatsSection.organizationStats();
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Métriques Organisation',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Text('Statistiques de l\'organisation à implémenter'),
],
),
);
}
/// Section actions rapides admin
@@ -482,8 +505,32 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
),
const SizedBox(height: SpacingTokens.md),
// Remplacé par PerformanceCard pour les métriques
const PerformanceCard.server(),
// Métriques serveur
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Performance Serveur',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Métriques serveur à implémenter'),
],
),
),
],
);
}
@@ -501,8 +548,32 @@ class _OrgAdminDashboardState extends State<OrgAdminDashboard> {
),
const SizedBox(height: SpacingTokens.md),
// Remplacé par RecentActivitiesSection
const RecentActivitiesSection.organization(),
// Activités récentes de l'organisation
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activités Récentes',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Activités de l\'organisation à implémenter'),
],
),
),
],
);
}

View File

@@ -3,8 +3,8 @@
library simple_member_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.dart';
import '../../widgets/widgets.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../widgets/dashboard_widgets.dart';
/// Dashboard Personal Space pour Membre Simple
class SimpleMemberDashboard extends StatelessWidget {
@@ -148,38 +148,33 @@ class SimpleMemberDashboard extends StatelessWidget {
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardStatsGrid(
const DashboardStatsGrid(
stats: [
DashboardStat(
icon: Icons.payment,
value: 'À jour',
title: 'Cotisations',
color: const Color(0xFF00B894),
onTap: () {},
color: Color(0xFF00B894),
),
DashboardStat(
icon: Icons.event,
value: '2',
title: 'Événements',
color: const Color(0xFF00CEC9),
onTap: () {},
color: Color(0xFF00CEC9),
),
DashboardStat(
icon: Icons.account_circle,
value: '100%',
title: 'Profil',
color: const Color(0xFF0984E3),
onTap: () {},
color: Color(0xFF0984E3),
),
DashboardStat(
icon: Icons.notifications,
value: '3',
title: 'Notifications',
color: const Color(0xFFE17055),
onTap: () {},
color: Color(0xFFE17055),
),
],
onStatTap: (type) {},
),
],
);
@@ -195,37 +190,32 @@ class SimpleMemberDashboard extends StatelessWidget {
),
const SizedBox(height: SpacingTokens.md),
DashboardQuickActionsGrid(
actions: [
children: [
DashboardQuickAction(
icon: Icons.edit,
title: 'Modifier Profil',
subtitle: 'Mes informations',
color: const Color(0xFF00CEC9),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.payment,
title: 'Mes Cotisations',
subtitle: 'Historique paiements',
color: const Color(0xFF0984E3),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.event,
title: 'Événements',
subtitle: 'Voir les événements',
color: const Color(0xFF00B894),
onTap: () {},
),
DashboardQuickAction(
icon: Icons.help,
title: 'Aide',
subtitle: 'Support & FAQ',
color: const Color(0xFFE17055),
onTap: () {},
),
],
onActionTap: (type) {},
),
],
);
@@ -339,8 +329,8 @@ class SimpleMemberDashboard extends StatelessWidget {
style: TypographyTokens.headlineMedium.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: SpacingTokens.md),
DashboardRecentActivitySection(
activities: const [
const DashboardRecentActivitySection(
children: [
DashboardActivity(
title: 'Cotisation payée',
subtitle: 'Décembre 2024',
@@ -363,7 +353,6 @@ class SimpleMemberDashboard extends StatelessWidget {
time: 'Il y a 2 sem',
),
],
onActivityTap: (id) {},
),
],
);

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import '../../widgets/dashboard_widgets.dart';
@@ -39,23 +38,131 @@ class _SuperAdminDashboardState extends State<SuperAdminDashboard> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec informations système
const DashboardHeader.superAdmin(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Super Admin Dashboard',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.red),
),
SizedBox(height: 8),
Text('Accès complet au système'),
],
),
),
const SizedBox(height: 16),
// KPIs système en temps réel
const QuickStatsSection.systemKPIs(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'KPIs Système',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Indicateurs système à implémenter'),
],
),
),
const SizedBox(height: 16),
// Performance serveur
const PerformanceCard.server(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Performance Serveur',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Métriques serveur à implémenter'),
],
),
),
const SizedBox(height: 16),
// Alertes importantes
const RecentActivitiesSection.alerts(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alertes Système',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange),
),
SizedBox(height: 8),
Text('Alertes importantes à implémenter'),
],
),
),
const SizedBox(height: 16),
// Activité récente
const RecentActivitiesSection.system(),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activité Système',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Activités système à implémenter'),
],
),
),
const SizedBox(height: 16),
// Actions rapides système

View File

@@ -3,7 +3,10 @@
library visitor_dashboard;
import 'package:flutter/material.dart';
import '../../../../../core/design_system/tokens/tokens.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 {

View File

@@ -1,250 +0,0 @@
# 🚀 Widgets Dashboard Améliorés - UnionFlow Mobile
## 📋 Vue d'ensemble
Cette documentation présente les **3 widgets dashboard améliorés** avec des fonctionnalités avancées, des styles multiples et une architecture moderne.
---
## 🎯 Widgets Améliorés
### 1. **DashboardQuickActionButton** - Boutons d'Action Sophistiqués
#### ✨ Nouvelles Fonctionnalités :
- **7 types d'actions** : `primary`, `secondary`, `success`, `warning`, `error`, `info`, `custom`
- **6 styles** : `elevated`, `filled`, `outlined`, `text`, `gradient`, `minimal`
- **4 tailles** : `small`, `medium`, `large`, `extraLarge`
- **5 états** : `enabled`, `disabled`, `loading`, `success`, `error`
- **Animations fluides** avec contrôle granulaire
- **Feedback haptique** configurable
- **Badges et indicateurs** visuels
- **Icônes secondaires** pour plus de contexte
- **Tooltips** avec descriptions détaillées
- **Support long press** pour actions avancées
#### 🎨 Constructeurs Spécialisés :
```dart
// Action primaire
DashboardQuickAction.primary(
icon: Icons.person_add,
title: 'Ajouter Membre',
subtitle: 'Nouveau',
badge: '+',
onTap: () => handleAction(),
)
// Action avec gradient
DashboardQuickAction.gradient(
icon: Icons.star,
title: 'Premium',
gradient: LinearGradient(...),
onTap: () => handlePremium(),
)
```
---
### 2. **DashboardQuickActionsGrid** - Grilles Flexibles et Responsives
#### ✨ Nouvelles Fonctionnalités :
- **7 layouts** : `grid2x2`, `grid3x2`, `grid4x2`, `horizontal`, `vertical`, `staggered`, `carousel`
- **5 styles** : `standard`, `compact`, `expanded`, `minimal`, `card`
- **Animations d'apparition** avec délais configurables
- **Filtrage par permissions** utilisateur
- **Limitation du nombre d'actions** affichées
- **Support "Voir tout"** pour navigation
- **Mode debug** pour développement
- **Responsive design** adaptatif
#### 🎨 Constructeurs Spécialisés :
```dart
// Grille compacte
DashboardQuickActionsGrid.compact(
title: 'Actions Rapides',
onActionTap: (type) => handleAction(type),
)
// Carrousel horizontal
DashboardQuickActionsGrid.carousel(
title: 'Actions Populaires',
animated: true,
)
// Grille étendue avec "Voir tout"
DashboardQuickActionsGrid.expanded(
title: 'Toutes les Actions',
subtitle: 'Accès complet',
onSeeAll: () => navigateToAllActions(),
)
```
---
### 3. **DashboardStatsCard** - Cartes de Statistiques Avancées
#### ✨ Nouvelles Fonctionnalités :
- **7 types de stats** : `count`, `percentage`, `currency`, `duration`, `rate`, `score`, `custom`
- **7 styles** : `standard`, `minimal`, `elevated`, `outlined`, `gradient`, `compact`, `detailed`
- **4 tailles** : `small`, `medium`, `large`, `extraLarge`
- **Indicateurs de tendance** : `up`, `down`, `stable`, `unknown`
- **Comparaisons temporelles** avec pourcentages de changement
- **Graphiques miniatures** (sparklines)
- **Badges et notifications** visuels
- **Formatage automatique** des valeurs
- **Animations d'apparition** sophistiquées
#### 🎨 Constructeurs Spécialisés :
```dart
// Statistique de comptage
DashboardStat.count(
icon: Icons.people,
value: '1,247',
title: 'Membres Actifs',
changePercentage: 12.5,
trend: StatTrend.up,
period: 'ce mois',
)
// Statistique avec devise
DashboardStat.currency(
icon: Icons.euro,
value: '45,230',
title: 'Revenus',
sparklineData: [100, 120, 110, 140, 135, 160],
style: StatCardStyle.detailed,
)
// Statistique avec gradient
DashboardStat.gradient(
icon: Icons.star,
value: '4.8',
title: 'Satisfaction',
gradient: LinearGradient(...),
)
```
---
## 🎯 Utilisation Pratique
### Import des Widgets :
```dart
import 'dashboard_quick_action_button.dart';
import 'dashboard_quick_actions_grid.dart';
import 'dashboard_stats_card.dart';
```
### Exemple d'Intégration :
```dart
Column(
children: [
// Grille d'actions rapides
DashboardQuickActionsGrid.expanded(
title: 'Actions Principales',
onActionTap: (type) => _handleQuickAction(type),
userPermissions: currentUser.permissions,
),
SizedBox(height: 20),
// Statistiques en grille
GridView.count(
crossAxisCount: 2,
children: [
DashboardStatsCard(
stat: DashboardStat.count(
icon: Icons.people,
value: '${memberCount}',
title: 'Membres',
changePercentage: memberGrowth,
trend: memberTrend,
),
),
// ... autres stats
],
),
],
)
```
---
## 🎨 Design System
### Couleurs Utilisées :
- **Primary** : `#6C5CE7` (Violet principal)
- **Success** : `#00B894` (Vert succès)
- **Warning** : `#FDCB6E` (Orange alerte)
- **Error** : `#E17055` (Rouge erreur)
### Espacements :
- **Small** : `8px`
- **Medium** : `16px`
- **Large** : `24px`
- **Extra Large** : `32px`
### Animations :
- **Durée standard** : `200ms`
- **Courbe** : `Curves.easeOutBack`
- **Délai entre éléments** : `100ms`
---
## 🧪 Test et Démonstration
### Page de Test :
```dart
import 'test_improved_widgets.dart';
// Navigation vers la page de test
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TestImprovedWidgetsPage(),
),
);
```
### Fonctionnalités Testées :
- ✅ Tous les styles et tailles
- ✅ Animations et transitions
- ✅ Feedback haptique
- ✅ Gestion des états
- ✅ Responsive design
- ✅ Accessibilité
---
## 📊 Métriques d'Amélioration
### Performance :
- **Réduction du code** : -60% de duplication
- **Temps de développement** : -75% pour nouveaux dashboards
- **Maintenance** : +80% plus facile
### Fonctionnalités :
- **Styles disponibles** : 6x plus qu'avant
- **Layouts supportés** : 7 types différents
- **États gérés** : 5 états interactifs
- **Animations** : 100% fluides et configurables
### Dimensions Optimisées :
- **Largeur des boutons** : Réduite de 50% (140px → 100px)
- **Hauteur des boutons** : Optimisée (100px → 70px)
- **Format rectangulaire** : Ratio d'aspect 1.6 au lieu de 2.2
- **Bordures** : Moins arrondies (12px → 6px)
- **Espacement** : Réduit pour plus de compacité
---
## 🚀 Prochaines Étapes
1. **Tests unitaires** complets
2. **Documentation API** détaillée
3. **Exemples d'usage** avancés
4. **Intégration** dans tous les dashboards
5. **Optimisations** de performance
---
**Les widgets dashboard UnionFlow Mobile sont maintenant de niveau professionnel avec une architecture moderne et des fonctionnalités avancées !** 🎯✨

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

@@ -1,9 +1,12 @@
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;
@@ -53,7 +56,7 @@ class ActivityItem extends StatelessWidget {
required this.timestamp,
this.onTap,
}) : icon = Icons.settings,
color = const Color(0xFF6C5CE7),
color = ColorTokens.primary,
type = ActivityType.system,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
@@ -66,7 +69,7 @@ class ActivityItem extends StatelessWidget {
required this.timestamp,
this.onTap,
}) : icon = Icons.person,
color = const Color(0xFF00B894),
color = ColorTokens.success,
type = ActivityType.user,
style = ActivityItemStyle.normal,
showStatusIndicator = true;
@@ -79,7 +82,7 @@ class ActivityItem extends StatelessWidget {
required this.timestamp,
this.onTap,
}) : icon = Icons.warning,
color = Colors.orange,
color = ColorTokens.warning,
type = ActivityType.alert,
style = ActivityItemStyle.alert,
showStatusIndicator = true;
@@ -339,24 +342,24 @@ class ActivityItem extends StatelessWidget {
/// Couleur effective selon le type
Color _getEffectiveColor() {
if (color != null) return color!;
switch (type) {
case ActivityType.system:
return const Color(0xFF6C5CE7);
return ColorTokens.primary;
case ActivityType.user:
return const Color(0xFF00B894);
return ColorTokens.success;
case ActivityType.organization:
return const Color(0xFF0984E3);
return ColorTokens.info;
case ActivityType.event:
return const Color(0xFFE17055);
return ColorTokens.secondary;
case ActivityType.alert:
return Colors.orange;
return ColorTokens.warning;
case ActivityType.error:
return Colors.red;
return ColorTokens.error;
case ActivityType.success:
return const Color(0xFF00B894);
return ColorTokens.success;
case null:
return const Color(0xFF6C5CE7);
return ColorTokens.primary;
}
}

View File

@@ -1,9 +1,12 @@
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;
@@ -48,7 +51,7 @@ class SectionHeader extends StatelessWidget {
this.subtitle,
this.action,
this.icon,
}) : color = const Color(0xFF6C5CE7),
}) : color = ColorTokens.primary,
fontSize = 20,
style = SectionHeaderStyle.primary,
bottomSpacing = 16;
@@ -60,7 +63,7 @@ class SectionHeader extends StatelessWidget {
this.subtitle,
this.action,
this.icon,
}) : color = const Color(0xFF6C5CE7),
}) : color = ColorTokens.primary,
fontSize = 16,
style = SectionHeaderStyle.normal,
bottomSpacing = 12;
@@ -100,25 +103,21 @@ class SectionHeader extends StatelessWidget {
/// En-tête principal avec fond coloré
Widget _buildPrimaryHeader() {
final effectiveColor = color ?? ColorTokens.primary;
return Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
color ?? const Color(0xFF6C5CE7),
(color ?? const Color(0xFF6C5CE7)).withOpacity(0.8),
effectiveColor,
effectiveColor.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: (color ?? const Color(0xFF6C5CE7)).withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
borderRadius: BorderRadius.circular(SpacingTokens.radiusLg),
boxShadow: ShadowTokens.primary,
),
child: Row(
children: [
@@ -175,10 +174,10 @@ class SectionHeader extends StatelessWidget {
if (icon != null) ...[
Icon(
icon,
color: color ?? const Color(0xFF6C5CE7),
color: color ?? ColorTokens.primary,
size: 20,
),
const SizedBox(width: 8),
const SizedBox(width: SpacingTokens.md),
],
Expanded(
child: Column(
@@ -189,7 +188,7 @@ class SectionHeader extends StatelessWidget {
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.bold,
color: color ?? const Color(0xFF6C5CE7),
color: color ?? ColorTokens.primary,
),
),
if (subtitle != null) ...[
@@ -257,10 +256,10 @@ class SectionHeader extends StatelessWidget {
if (icon != null) ...[
Icon(
icon,
color: color ?? const Color(0xFF6C5CE7),
color: color ?? ColorTokens.primary,
size: 20,
),
const SizedBox(width: 8),
const SizedBox(width: SpacingTokens.md),
],
Expanded(
child: Column(
@@ -271,7 +270,7 @@ class SectionHeader extends StatelessWidget {
style: TextStyle(
fontSize: fontSize ?? 16,
fontWeight: FontWeight.bold,
color: color ?? const Color(0xFF6C5CE7),
color: color ?? ColorTokens.primary,
),
),
if (subtitle != null) ...[

View File

@@ -1,9 +1,12 @@
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;
@@ -48,21 +51,21 @@ class PerformanceCard extends StatelessWidget {
label: 'CPU',
value: 67.3,
unit: '%',
color: Colors.orange,
color: ColorTokens.warning,
threshold: 80,
),
PerformanceMetric(
label: 'RAM',
value: 78.5,
unit: '%',
color: Colors.blue,
color: ColorTokens.info,
threshold: 85,
),
PerformanceMetric(
label: 'Disque',
value: 45.2,
unit: '%',
color: Colors.green,
color: ColorTokens.success,
threshold: 90,
),
],
@@ -81,21 +84,21 @@ class PerformanceCard extends StatelessWidget {
label: 'Latence',
value: 12.0,
unit: 'ms',
color: Color(0xFF00B894),
color: ColorTokens.success,
threshold: 100.0,
),
PerformanceMetric(
label: 'Débit',
value: 85.0,
unit: 'Mbps',
color: Color(0xFF6C5CE7),
color: ColorTokens.primary,
threshold: 100.0,
),
PerformanceMetric(
label: 'Paquets perdus',
value: 0.2,
unit: '%',
color: Color(0xFFE17055),
color: ColorTokens.secondary,
threshold: 5.0,
),
],
@@ -107,14 +110,13 @@ class PerformanceCard extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: _getDecoration(),
child: UFCard(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 12),
const SizedBox(height: SpacingTokens.lg),
_buildMetrics(),
],
),
@@ -129,19 +131,17 @@ class PerformanceCard extends StatelessWidget {
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
style: TypographyTokens.titleMedium.copyWith(
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
color: ColorTokens.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
@@ -153,7 +153,7 @@ class PerformanceCard extends StatelessWidget {
Widget _buildMetrics() {
return Column(
children: metrics.map((metric) => Padding(
padding: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.only(bottom: SpacingTokens.md),
child: _buildMetricRow(metric),
)).toList(),
);
@@ -163,12 +163,12 @@ class PerformanceCard extends StatelessWidget {
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 = Colors.red;
effectiveColor = ColorTokens.error;
} else if (isWarning) {
effectiveColor = Colors.orange;
effectiveColor = ColorTokens.warning;
}
return Column(
@@ -183,28 +183,26 @@ class PerformanceCard extends StatelessWidget {
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
const SizedBox(width: SpacingTokens.md),
Text(
metric.label,
style: const TextStyle(
style: TypographyTokens.labelMedium.copyWith(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
const Spacer(),
if (showValues)
Text(
'${metric.value.toStringAsFixed(1)}${metric.unit}',
style: TextStyle(
style: TypographyTokens.labelMedium.copyWith(
color: effectiveColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
],
),
if (showProgressBars) ...[
const SizedBox(height: 4),
const SizedBox(height: SpacingTokens.xs),
_buildProgressBar(metric, effectiveColor),
],
],
@@ -214,12 +212,12 @@ class PerformanceCard extends StatelessWidget {
/// 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: Colors.grey[200],
borderRadius: BorderRadius.circular(2),
color: ColorTokens.surfaceVariant,
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
@@ -227,44 +225,14 @@ class PerformanceCard extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
borderRadius: BorderRadius.circular(SpacingTokens.radiusXs),
),
),
),
);
}
/// Décoration selon le style
BoxDecoration _getDecoration() {
switch (style) {
case PerformanceCardStyle.elevated:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
);
case PerformanceCardStyle.outlined:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF6C5CE7).withOpacity(0.2),
width: 1,
),
);
case PerformanceCardStyle.minimal:
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
);
}
}
}
/// Modèle de données pour une métrique de performance

View File

@@ -0,0 +1,342 @@
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 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: () {
// TODO: Naviguer vers l'action
},
icon: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: DashboardTheme.grey400,
),
),
],
);
}
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

@@ -1,102 +0,0 @@
/// Widget de tuile d'activité individuelle
/// Affiche une activité récente avec icône, titre et timestamp
library dashboard_activity_tile;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une activité récente
class DashboardActivity {
/// Titre principal de l'activité
final String title;
/// Description détaillée de l'activité
final String subtitle;
/// Icône représentative de l'activité
final IconData icon;
/// Couleur thématique de l'activité
final Color color;
/// Timestamp de l'activité
final String time;
/// Callback optionnel lors du tap sur l'activité
final VoidCallback? onTap;
/// Constructeur du modèle d'activité
const DashboardActivity({
required this.title,
required this.subtitle,
required this.icon,
required this.color,
required this.time,
this.onTap,
});
}
/// Widget de tuile d'activité
///
/// Affiche une activité récente avec :
/// - Avatar coloré avec icône thématique
/// - Titre et description de l'activité
/// - Timestamp relatif
/// - Design compact et lisible
/// - Support du tap pour détails
class DashboardActivityTile extends StatelessWidget {
/// Données de l'activité à afficher
final DashboardActivity activity;
/// Constructeur de la tuile d'activité
const DashboardActivityTile({
super.key,
required this.activity,
});
@override
Widget build(BuildContext context) {
return ListTile(
onTap: activity.onTap,
contentPadding: const EdgeInsets.symmetric(
horizontal: SpacingTokens.sm,
vertical: SpacingTokens.xs,
),
leading: CircleAvatar(
radius: 16,
backgroundColor: activity.color.withOpacity(0.1),
child: Icon(
activity.icon,
color: activity.color,
size: 16,
),
),
title: Text(
activity.title,
style: TypographyTokens.bodySmall.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
activity.subtitle,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
fontSize: 12,
),
),
trailing: SizedBox(
width: 60,
child: Text(
activity.time,
style: TypographyTokens.labelSmall.copyWith(
color: ColorTokens.onSurfaceVariant,
fontSize: 11,
),
textAlign: TextAlign.end,
),
),
);
}
}

View File

@@ -3,9 +3,9 @@
library dashboard_drawer;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.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 {

View File

@@ -1,359 +0,0 @@
import 'package:flutter/material.dart';
import 'common/section_header.dart';
/// Widget d'en-tête principal du dashboard
///
/// Composant réutilisable pour l'en-tête des dashboards avec
/// informations système, statut et actions rapides.
class DashboardHeader extends StatelessWidget {
/// Titre principal du dashboard
final String title;
/// Sous-titre ou description
final String? subtitle;
/// Afficher les informations système
final bool showSystemInfo;
/// Afficher les actions rapides
final bool showQuickActions;
/// Callback pour les actions personnalisées
final List<DashboardAction>? actions;
/// Métriques système à afficher
final List<SystemMetric>? systemMetrics;
/// Style de l'en-tête
final DashboardHeaderStyle style;
const DashboardHeader({
super.key,
required this.title,
this.subtitle,
this.showSystemInfo = true,
this.showQuickActions = true,
this.actions,
this.systemMetrics,
this.style = DashboardHeaderStyle.gradient,
});
/// Constructeur pour un en-tête Super Admin
const DashboardHeader.superAdmin({
super.key,
this.actions,
}) : title = 'Administration Système',
subtitle = 'Surveillance et gestion globale',
showSystemInfo = true,
showQuickActions = true,
systemMetrics = null,
style = DashboardHeaderStyle.gradient;
/// Constructeur pour un en-tête Admin Organisation
const DashboardHeader.orgAdmin({
super.key,
this.actions,
}) : title = 'Administration Organisation',
subtitle = 'Gestion de votre organisation',
showSystemInfo = false,
showQuickActions = true,
systemMetrics = null,
style = DashboardHeaderStyle.gradient;
/// Constructeur pour un en-tête Membre
const DashboardHeader.member({
super.key,
this.actions,
}) : title = 'Tableau de bord',
subtitle = 'Bienvenue dans UnionFlow',
showSystemInfo = false,
showQuickActions = false,
systemMetrics = null,
style = DashboardHeaderStyle.simple;
@override
Widget build(BuildContext context) {
switch (style) {
case DashboardHeaderStyle.gradient:
return _buildGradientHeader();
case DashboardHeaderStyle.simple:
return _buildSimpleHeader();
case DashboardHeaderStyle.card:
return _buildCardHeader();
}
}
/// En-tête avec gradient (style principal)
Widget _buildGradientHeader() {
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF6C5CE7).withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderContent(),
if (showSystemInfo && systemMetrics != null) ...[
const SizedBox(height: 16),
_buildSystemMetrics(),
],
if (showQuickActions && actions != null) ...[
const SizedBox(height: 16),
_buildQuickActions(),
],
],
),
);
}
/// En-tête simple sans fond
Widget _buildSimpleHeader() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader.primary(
title: title,
subtitle: subtitle,
action: actions?.isNotEmpty == true ? _buildActionsRow() : null,
),
],
),
);
}
/// En-tête avec fond de carte
Widget _buildCardHeader() {
return Container(
margin: const EdgeInsets.all(12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderContent(isWhiteBackground: true),
if (showSystemInfo && systemMetrics != null) ...[
const SizedBox(height: 16),
_buildSystemMetrics(isWhiteBackground: true),
],
],
),
);
}
/// Contenu principal de l'en-tête
Widget _buildHeaderContent({bool isWhiteBackground = false}) {
final textColor = isWhiteBackground ? const Color(0xFF1F2937) : Colors.white;
final subtitleColor = isWhiteBackground ? Colors.grey[600] : Colors.white.withOpacity(0.8);
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: textColor,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: TextStyle(
fontSize: 16,
color: subtitleColor,
),
),
],
],
),
),
if (actions?.isNotEmpty == true) _buildActionsRow(isWhiteBackground: isWhiteBackground),
],
);
}
/// Métriques système
Widget _buildSystemMetrics({bool isWhiteBackground = false}) {
if (systemMetrics == null || systemMetrics!.isEmpty) {
return _buildDefaultSystemMetrics(isWhiteBackground: isWhiteBackground);
}
return Wrap(
spacing: 12,
runSpacing: 8,
children: systemMetrics!.map((metric) => _buildMetricChip(
metric.label,
metric.value,
metric.icon,
isWhiteBackground: isWhiteBackground,
)).toList(),
);
}
/// Métriques système par défaut
Widget _buildDefaultSystemMetrics({bool isWhiteBackground = false}) {
return Row(
children: [
Expanded(child: _buildMetricChip('Uptime', '99.97%', Icons.trending_up, isWhiteBackground: isWhiteBackground)),
const SizedBox(width: 12),
Expanded(child: _buildMetricChip('CPU', '23%', Icons.memory, isWhiteBackground: isWhiteBackground)),
const SizedBox(width: 12),
Expanded(child: _buildMetricChip('Users', '1,247', Icons.people, isWhiteBackground: isWhiteBackground)),
],
);
}
/// Chip de métrique
Widget _buildMetricChip(String label, String value, IconData icon, {bool isWhiteBackground = false}) {
final backgroundColor = isWhiteBackground
? const Color(0xFF6C5CE7).withOpacity(0.1)
: Colors.white.withOpacity(0.15);
final textColor = isWhiteBackground ? const Color(0xFF6C5CE7) : Colors.white;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: textColor, size: 16),
const SizedBox(width: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: textColor,
),
),
Text(
label,
style: TextStyle(
fontSize: 10,
color: textColor.withOpacity(0.8),
),
),
],
),
],
),
);
}
/// Actions rapides
Widget _buildQuickActions({bool isWhiteBackground = false}) {
if (actions == null || actions!.isEmpty) return const SizedBox.shrink();
return Row(
children: actions!.map((action) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: _buildActionButton(action, isWhiteBackground: isWhiteBackground),
),
)).toList(),
);
}
/// Ligne d'actions
Widget _buildActionsRow({bool isWhiteBackground = false}) {
if (actions == null || actions!.isEmpty) return const SizedBox.shrink();
return Row(
mainAxisSize: MainAxisSize.min,
children: actions!.map((action) => Padding(
padding: const EdgeInsets.only(left: 8),
child: _buildActionButton(action, isWhiteBackground: isWhiteBackground),
)).toList(),
);
}
/// Bouton d'action
Widget _buildActionButton(DashboardAction action, {bool isWhiteBackground = false}) {
final backgroundColor = isWhiteBackground
? Colors.white
: Colors.white.withOpacity(0.2);
final iconColor = isWhiteBackground ? const Color(0xFF6C5CE7) : Colors.white;
return Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: IconButton(
onPressed: action.onPressed,
icon: Icon(action.icon, color: iconColor),
tooltip: action.tooltip,
),
);
}
}
/// Action du dashboard
class DashboardAction {
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
const DashboardAction({
required this.icon,
required this.tooltip,
required this.onPressed,
});
}
/// Métrique système
class SystemMetric {
final String label;
final String value;
final IconData icon;
const SystemMetric({
required this.label,
required this.value,
required this.icon,
});
}
/// Styles d'en-tête de dashboard
enum DashboardHeaderStyle {
gradient,
simple,
card,
}

View File

@@ -1,104 +0,0 @@
/// Widget de section d'insights du dashboard
/// Affiche les métriques de performance dans une carte
library dashboard_insights_section;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_metric_row.dart';
/// Widget de section d'insights
///
/// Affiche les métriques de performance :
/// - Taux de cotisation
/// - Participation aux événements
/// - Demandes traitées
///
/// Chaque métrique peut être tapée pour plus de détails
class DashboardInsightsSection extends StatelessWidget {
/// Callback pour les actions sur les métriques
final Function(String metricType)? onMetricTap;
/// Liste des métriques à afficher
final List<DashboardMetric>? metrics;
/// Constructeur de la section d'insights
const DashboardInsightsSection({
super.key,
this.onMetricTap,
this.metrics,
});
/// Génère la liste des métriques par défaut
List<DashboardMetric> _getDefaultMetrics() {
return [
DashboardMetric(
label: 'Taux de cotisation',
value: '85%',
progress: 0.85,
color: ColorTokens.success,
onTap: () => onMetricTap?.call('cotisation_rate'),
),
DashboardMetric(
label: 'Participation événements',
value: '72%',
progress: 0.72,
color: ColorTokens.primary,
onTap: () => onMetricTap?.call('event_participation'),
),
DashboardMetric(
label: 'Demandes traitées',
value: '95%',
progress: 0.95,
color: ColorTokens.tertiary,
onTap: () => onMetricTap?.call('requests_processed'),
),
];
}
@override
Widget build(BuildContext context) {
final metricsToShow = metrics ?? _getDefaultMetrics();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Insights',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Performance ce mois-ci',
style: TypographyTokens.titleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.md),
...metricsToShow.map((metric) {
final isLast = metric == metricsToShow.last;
return Column(
children: [
DashboardMetricRow(metric: metric),
if (!isLast) const SizedBox(height: SpacingTokens.sm),
],
);
}),
],
),
),
),
],
);
}
}

View File

@@ -1,93 +0,0 @@
/// Widget de ligne de métrique avec barre de progression
/// Affiche une métrique avec label, valeur et indicateur visuel
library dashboard_metric_row;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Modèle de données pour une métrique
class DashboardMetric {
/// Label descriptif de la métrique
final String label;
/// Valeur formatée à afficher
final String value;
/// Progression entre 0.0 et 1.0
final double progress;
/// Couleur thématique de la métrique
final Color color;
/// Callback optionnel lors du tap sur la métrique
final VoidCallback? onTap;
/// Constructeur du modèle de métrique
const DashboardMetric({
required this.label,
required this.value,
required this.progress,
required this.color,
this.onTap,
});
}
/// Widget de ligne de métrique
///
/// Affiche une métrique avec :
/// - Label et valeur alignés horizontalement
/// - Barre de progression colorée
/// - Design compact et lisible
/// - Support du tap pour détails
class DashboardMetricRow extends StatelessWidget {
/// Données de la métrique à afficher
final DashboardMetric metric;
/// Constructeur de la ligne de métrique
const DashboardMetricRow({
super.key,
required this.metric,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: metric.onTap,
borderRadius: BorderRadius.circular(SpacingTokens.radiusSm),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.xs),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
metric.label,
style: TypographyTokens.bodySmall.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
metric.value,
style: TypographyTokens.labelLarge.copyWith(
fontWeight: FontWeight.w600,
color: metric.color,
),
),
],
),
const SizedBox(height: SpacingTokens.xs),
LinearProgressIndicator(
value: metric.progress,
backgroundColor: metric.color.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(metric.color),
minHeight: 4,
),
],
),
),
);
}
}

View File

@@ -1,683 +0,0 @@
/// Widget de bouton d'action rapide individuel - Version Améliorée
/// Bouton stylisé sophistiqué pour les actions principales du dashboard
/// avec support d'animations, badges, états et styles multiples
library dashboard_quick_action_button;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
/// Types d'actions rapides disponibles
enum QuickActionType {
primary,
secondary,
success,
warning,
error,
info,
custom,
}
/// Styles de boutons d'action rapide
enum QuickActionStyle {
elevated,
filled,
outlined,
text,
gradient,
minimal,
}
/// Tailles de boutons d'action rapide
enum QuickActionSize {
small,
medium,
large,
extraLarge,
}
/// États du bouton d'action rapide
enum QuickActionState {
enabled,
disabled,
loading,
success,
error,
}
/// Modèle de données avancé pour une action rapide
class DashboardQuickAction {
/// Icône représentative de l'action
final IconData icon;
/// Titre de l'action
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Description détaillée (tooltip)
final String? description;
/// Couleur thématique du bouton
final Color color;
/// Type d'action (détermine le style par défaut)
final QuickActionType type;
/// Style du bouton
final QuickActionStyle style;
/// Taille du bouton
final QuickActionSize size;
/// État actuel du bouton
final QuickActionState state;
/// Callback lors du tap sur le bouton
final VoidCallback? onTap;
/// Callback lors du long press
final VoidCallback? onLongPress;
/// Badge à afficher (nombre ou texte)
final String? badge;
/// Couleur du badge
final Color? badgeColor;
/// Icône secondaire (affichée en bas à droite)
final IconData? secondaryIcon;
/// Gradient personnalisé
final Gradient? gradient;
/// Animation activée
final bool animated;
/// Feedback haptique activé
final bool hapticFeedback;
/// Constructeur du modèle d'action rapide amélioré
const DashboardQuickAction({
required this.icon,
required this.title,
this.subtitle,
this.description,
required this.color,
this.type = QuickActionType.primary,
this.style = QuickActionStyle.elevated,
this.size = QuickActionSize.medium,
this.state = QuickActionState.enabled,
this.onTap,
this.onLongPress,
this.badge,
this.badgeColor,
this.secondaryIcon,
this.gradient,
this.animated = true,
this.hapticFeedback = true,
});
/// Constructeur pour action primaire
const DashboardQuickAction.primary({
required this.icon,
required this.title,
this.subtitle,
this.description,
this.onTap,
this.onLongPress,
this.badge,
this.size = QuickActionSize.medium,
this.state = QuickActionState.enabled,
this.animated = true,
this.hapticFeedback = true,
}) : color = ColorTokens.primary,
type = QuickActionType.primary,
style = QuickActionStyle.elevated,
badgeColor = null,
secondaryIcon = null,
gradient = null;
/// Constructeur pour action de succès
const DashboardQuickAction.success({
required this.icon,
required this.title,
this.subtitle,
this.description,
this.onTap,
this.onLongPress,
this.badge,
this.size = QuickActionSize.medium,
this.state = QuickActionState.enabled,
this.animated = true,
this.hapticFeedback = true,
}) : color = ColorTokens.success,
type = QuickActionType.success,
style = QuickActionStyle.filled,
badgeColor = null,
secondaryIcon = null,
gradient = null;
/// Constructeur pour action d'alerte
const DashboardQuickAction.warning({
required this.icon,
required this.title,
this.subtitle,
this.description,
this.onTap,
this.onLongPress,
this.badge,
this.size = QuickActionSize.medium,
this.state = QuickActionState.enabled,
this.animated = true,
this.hapticFeedback = true,
}) : color = ColorTokens.warning,
type = QuickActionType.warning,
style = QuickActionStyle.outlined,
badgeColor = null,
secondaryIcon = null,
gradient = null;
/// Constructeur pour action avec gradient
const DashboardQuickAction.gradient({
required this.icon,
required this.title,
this.subtitle,
this.description,
required this.gradient,
this.onTap,
this.onLongPress,
this.badge,
this.size = QuickActionSize.medium,
this.state = QuickActionState.enabled,
this.animated = true,
this.hapticFeedback = true,
}) : color = ColorTokens.primary,
type = QuickActionType.custom,
style = QuickActionStyle.gradient,
badgeColor = null,
secondaryIcon = null;
}
/// Widget de bouton d'action rapide amélioré
///
/// Affiche un bouton stylisé sophistiqué avec :
/// - Icône thématique avec animations
/// - Titre et sous-titre descriptifs
/// - Badges et indicateurs visuels
/// - Styles multiples (elevated, filled, outlined, gradient)
/// - États interactifs (loading, success, error)
/// - Feedback haptique et animations
/// - Support tooltip et long press
/// - Design Material 3 avec bordures arrondies
class DashboardQuickActionButton extends StatefulWidget {
/// Données de l'action à afficher
final DashboardQuickAction action;
/// Constructeur du bouton d'action rapide amélioré
const DashboardQuickActionButton({
super.key,
required this.action,
});
@override
State<DashboardQuickActionButton> createState() => _DashboardQuickActionButtonState();
}
class _DashboardQuickActionButtonState extends State<DashboardQuickActionButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.95,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 0.1,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// Obtient les dimensions selon la taille (format rectangulaire compact)
EdgeInsets _getPadding() {
switch (widget.action.size) {
case QuickActionSize.small:
return const EdgeInsets.symmetric(horizontal: SpacingTokens.xs, vertical: SpacingTokens.xs);
case QuickActionSize.medium:
return const EdgeInsets.symmetric(horizontal: SpacingTokens.sm, vertical: SpacingTokens.sm);
case QuickActionSize.large:
return const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.sm);
case QuickActionSize.extraLarge:
return const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.md);
}
}
/// Obtient la taille de l'icône selon la taille du bouton (réduite pour format compact)
double _getIconSize() {
switch (widget.action.size) {
case QuickActionSize.small:
return 14.0;
case QuickActionSize.medium:
return 16.0;
case QuickActionSize.large:
return 18.0;
case QuickActionSize.extraLarge:
return 20.0;
}
}
/// Obtient le style de texte pour le titre
TextStyle _getTitleStyle() {
final baseSize = widget.action.size == QuickActionSize.small ? 11.0 :
widget.action.size == QuickActionSize.medium ? 12.0 :
widget.action.size == QuickActionSize.large ? 13.0 : 14.0;
return TextStyle(
fontWeight: FontWeight.w600,
fontSize: baseSize,
color: _getTextColor(),
);
}
/// Obtient le style de texte pour le sous-titre
TextStyle _getSubtitleStyle() {
final baseSize = widget.action.size == QuickActionSize.small ? 9.0 :
widget.action.size == QuickActionSize.medium ? 10.0 :
widget.action.size == QuickActionSize.large ? 11.0 : 12.0;
return TextStyle(
fontSize: baseSize,
color: _getTextColor().withOpacity(0.7),
);
}
/// Obtient la couleur du texte selon le style
Color _getTextColor() {
switch (widget.action.style) {
case QuickActionStyle.filled:
case QuickActionStyle.gradient:
return Colors.white;
case QuickActionStyle.elevated:
case QuickActionStyle.outlined:
case QuickActionStyle.text:
case QuickActionStyle.minimal:
return widget.action.color;
}
}
/// Gère le tap avec feedback haptique
void _handleTap() {
if (widget.action.state != QuickActionState.enabled) return;
if (widget.action.hapticFeedback) {
HapticFeedback.lightImpact();
}
if (widget.action.animated) {
_animationController.forward().then((_) {
_animationController.reverse();
});
}
widget.action.onTap?.call();
}
/// Gère le long press
void _handleLongPress() {
if (widget.action.state != QuickActionState.enabled) return;
if (widget.action.hapticFeedback) {
HapticFeedback.mediumImpact();
}
widget.action.onLongPress?.call();
}
@override
Widget build(BuildContext context) {
Widget button = _buildButton();
// Ajouter tooltip si description fournie
if (widget.action.description != null) {
button = Tooltip(
message: widget.action.description!,
child: button,
);
}
// Ajouter animation si activée
if (widget.action.animated) {
button = AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: child,
),
);
},
child: button,
);
}
return button;
}
/// Construit le bouton selon le style défini
Widget _buildButton() {
switch (widget.action.style) {
case QuickActionStyle.elevated:
return _buildElevatedButton();
case QuickActionStyle.filled:
return _buildFilledButton();
case QuickActionStyle.outlined:
return _buildOutlinedButton();
case QuickActionStyle.text:
return _buildTextButton();
case QuickActionStyle.gradient:
return _buildGradientButton();
case QuickActionStyle.minimal:
return _buildMinimalButton();
}
}
/// Construit un bouton élevé
Widget _buildElevatedButton() {
return ElevatedButton(
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
style: ElevatedButton.styleFrom(
backgroundColor: widget.action.color.withOpacity(0.1),
foregroundColor: widget.action.color,
elevation: widget.action.state == QuickActionState.enabled ? 2 : 0,
padding: _getPadding(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
child: _buildButtonContent(),
);
}
/// Construit un bouton rempli
Widget _buildFilledButton() {
return ElevatedButton(
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
style: ElevatedButton.styleFrom(
backgroundColor: widget.action.color,
foregroundColor: Colors.white,
elevation: 0,
padding: _getPadding(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
child: _buildButtonContent(),
);
}
/// Construit un bouton avec contour
Widget _buildOutlinedButton() {
return OutlinedButton(
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
style: OutlinedButton.styleFrom(
foregroundColor: widget.action.color,
side: BorderSide(color: widget.action.color, width: 1.5),
padding: _getPadding(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
child: _buildButtonContent(),
);
}
/// Construit un bouton texte
Widget _buildTextButton() {
return TextButton(
onPressed: widget.action.state == QuickActionState.enabled ? _handleTap : null,
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
style: TextButton.styleFrom(
foregroundColor: widget.action.color,
padding: _getPadding(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
child: _buildButtonContent(),
);
}
/// Construit un bouton avec gradient
Widget _buildGradientButton() {
return Container(
decoration: BoxDecoration(
gradient: widget.action.gradient ?? LinearGradient(
colors: [widget.action.color, widget.action.color.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(6.0),
boxShadow: [
BoxShadow(
color: widget.action.color.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.action.state == QuickActionState.enabled ? _handleTap : null,
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
borderRadius: BorderRadius.circular(6.0),
child: Padding(
padding: _getPadding(),
child: _buildButtonContent(),
),
),
),
);
}
/// Construit un bouton minimal
Widget _buildMinimalButton() {
return InkWell(
onTap: widget.action.state == QuickActionState.enabled ? _handleTap : null,
onLongPress: widget.action.state == QuickActionState.enabled ? _handleLongPress : null,
borderRadius: BorderRadius.circular(6.0),
child: Container(
padding: _getPadding(),
decoration: BoxDecoration(
color: widget.action.color.withOpacity(0.05),
borderRadius: BorderRadius.circular(6.0),
border: Border.all(
color: widget.action.color.withOpacity(0.2),
width: 1,
),
),
child: _buildButtonContent(),
),
);
}
/// Construit le contenu du bouton (icône, texte, badge)
Widget _buildButtonContent() {
return Stack(
clipBehavior: Clip.none,
children: [
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildIcon(),
const SizedBox(height: 6),
_buildTitle(),
if (widget.action.subtitle != null) ...[
const SizedBox(height: 2),
_buildSubtitle(),
],
],
),
// Badge en haut à droite
if (widget.action.badge != null)
Positioned(
top: -8,
right: -8,
child: _buildBadge(),
),
// Icône secondaire en bas à droite
if (widget.action.secondaryIcon != null)
Positioned(
bottom: -4,
right: -4,
child: _buildSecondaryIcon(),
),
],
);
}
/// Construit l'icône principale avec état
Widget _buildIcon() {
IconData iconToShow = widget.action.icon;
// Changer l'icône selon l'état
switch (widget.action.state) {
case QuickActionState.loading:
return SizedBox(
width: _getIconSize(),
height: _getIconSize(),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(_getTextColor()),
),
);
case QuickActionState.success:
iconToShow = Icons.check_circle;
break;
case QuickActionState.error:
iconToShow = Icons.error;
break;
case QuickActionState.disabled:
case QuickActionState.enabled:
break;
}
return Icon(
iconToShow,
size: _getIconSize(),
color: _getTextColor().withOpacity(
widget.action.state == QuickActionState.disabled ? 0.5 : 1.0,
),
);
}
/// Construit le titre
Widget _buildTitle() {
return Text(
widget.action.title,
style: _getTitleStyle().copyWith(
color: _getTitleStyle().color?.withOpacity(
widget.action.state == QuickActionState.disabled ? 0.5 : 1.0,
),
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
/// Construit le sous-titre
Widget _buildSubtitle() {
return Text(
widget.action.subtitle!,
style: _getSubtitleStyle().copyWith(
color: _getSubtitleStyle().color?.withOpacity(
widget.action.state == QuickActionState.disabled ? 0.5 : 1.0,
),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
/// Construit le badge
Widget _buildBadge() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: widget.action.badgeColor ?? ColorTokens.error,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Text(
widget.action.badge!,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
);
}
/// Construit l'icône secondaire
Widget _buildSecondaryIcon() {
return Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: widget.action.color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Icon(
widget.action.secondaryIcon!,
size: 12,
color: Colors.white,
),
);
}
}

View File

@@ -1,542 +0,0 @@
/// Widget de grille d'actions rapides du dashboard - Version Améliorée
/// Affiche les actions principales dans une grille responsive et configurable
/// avec support d'animations, layouts multiples et personnalisation avancée
library dashboard_quick_actions_grid;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_quick_action_button.dart';
/// Types de layout pour la grille d'actions
enum QuickActionsLayout {
grid2x2,
grid3x2,
grid4x2,
horizontal,
vertical,
staggered,
carousel,
}
/// Styles de la grille d'actions
enum QuickActionsGridStyle {
standard,
compact,
expanded,
minimal,
card,
}
/// Widget de grille d'actions rapides amélioré
///
/// Affiche les actions principales dans différents layouts :
/// - Grille 2x2, 3x2, 4x2
/// - Layout horizontal ou vertical
/// - Grille décalée (staggered)
/// - Carrousel horizontal
///
/// Fonctionnalités avancées :
/// - Animations d'apparition
/// - Personnalisation complète
/// - Gestion des permissions
/// - Analytics intégrés
/// - Support responsive
class DashboardQuickActionsGrid extends StatefulWidget {
/// Callback pour les actions rapides
final Function(String actionType)? onActionTap;
/// Liste des actions à afficher
final List<DashboardQuickAction>? actions;
/// Layout de la grille
final QuickActionsLayout layout;
/// Style de la grille
final QuickActionsGridStyle style;
/// Titre de la section
final String? title;
/// Sous-titre de la section
final String? subtitle;
/// Afficher le titre
final bool showTitle;
/// Afficher les animations
final bool animated;
/// Délai entre les animations (en millisecondes)
final int animationDelay;
/// Nombre maximum d'actions à afficher
final int? maxActions;
/// Espacement entre les éléments
final double? spacing;
/// Ratio d'aspect des boutons
final double? aspectRatio;
/// Callback pour voir toutes les actions
final VoidCallback? onSeeAll;
/// Permissions utilisateur (pour filtrer les actions)
final List<String>? userPermissions;
/// Mode de débogage (affiche des infos supplémentaires)
final bool debugMode;
/// Constructeur de la grille d'actions rapides améliorée
const DashboardQuickActionsGrid({
super.key,
this.onActionTap,
this.actions,
this.layout = QuickActionsLayout.grid2x2,
this.style = QuickActionsGridStyle.standard,
this.title,
this.subtitle,
this.showTitle = true,
this.animated = true,
this.animationDelay = 100,
this.maxActions,
this.spacing,
this.aspectRatio,
this.onSeeAll,
this.userPermissions,
this.debugMode = false,
});
/// Constructeur pour grille compacte avec format rectangulaire
const DashboardQuickActionsGrid.compact({
super.key,
this.onActionTap,
this.actions,
this.title,
this.userPermissions,
}) : layout = QuickActionsLayout.grid2x2,
style = QuickActionsGridStyle.compact,
subtitle = null,
showTitle = true,
animated = false,
animationDelay = 0,
maxActions = 4,
spacing = null,
aspectRatio = 1.8, // Ratio rectangulaire compact
onSeeAll = null,
debugMode = false;
/// Constructeur pour carrousel horizontal avec format rectangulaire
const DashboardQuickActionsGrid.carousel({
super.key,
this.onActionTap,
this.actions,
this.title,
this.animated = true,
this.userPermissions,
}) : layout = QuickActionsLayout.carousel,
style = QuickActionsGridStyle.standard,
subtitle = null,
showTitle = true,
animationDelay = 150,
maxActions = null,
spacing = 8.0, // Espacement réduit
aspectRatio = 1.0, // Ratio plus carré pour format rectangulaire
onSeeAll = null,
debugMode = false;
/// Constructeur pour layout étendu avec format rectangulaire
const DashboardQuickActionsGrid.expanded({
super.key,
this.onActionTap,
this.actions,
this.title,
this.subtitle,
this.onSeeAll,
this.userPermissions,
}) : layout = QuickActionsLayout.grid3x2,
style = QuickActionsGridStyle.expanded,
showTitle = true,
animated = true,
animationDelay = 80,
maxActions = 6,
spacing = null,
aspectRatio = 1.5, // Ratio rectangulaire pour layout étendu
debugMode = false;
@override
State<DashboardQuickActionsGrid> createState() => _DashboardQuickActionsGridState();
}
class _DashboardQuickActionsGridState extends State<DashboardQuickActionsGrid>
with TickerProviderStateMixin {
late AnimationController _animationController;
late List<Animation<double>> _itemAnimations;
List<DashboardQuickAction> _filteredActions = [];
@override
void initState() {
super.initState();
_setupAnimations();
_filterActions();
}
@override
void didUpdateWidget(DashboardQuickActionsGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.actions != widget.actions ||
oldWidget.userPermissions != widget.userPermissions) {
_filterActions();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// Configure les animations
void _setupAnimations() {
_animationController = AnimationController(
duration: Duration(milliseconds: widget.animationDelay * 6),
vsync: this,
);
if (widget.animated) {
_animationController.forward();
}
}
/// Filtre les actions selon les permissions
void _filterActions() {
final actions = widget.actions ?? _getDefaultActions();
_filteredActions = actions.where((action) {
// Filtrer selon les permissions si définies
if (widget.userPermissions != null) {
// Logique de filtrage basée sur les permissions
// À implémenter selon les besoins spécifiques
return true;
}
return true;
}).toList();
// Limiter le nombre d'actions si spécifié
if (widget.maxActions != null && _filteredActions.length > widget.maxActions!) {
_filteredActions = _filteredActions.take(widget.maxActions!).toList();
}
// Recréer les animations pour le nouveau nombre d'éléments
_itemAnimations = List.generate(
_filteredActions.length,
(index) => Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Interval(
index * 0.1,
(index * 0.1) + 0.6,
curve: Curves.easeOutBack,
),
)),
);
if (mounted) setState(() {});
}
/// Génère la liste des actions rapides par défaut
List<DashboardQuickAction> _getDefaultActions() {
return [
DashboardQuickAction.primary(
icon: Icons.person_add,
title: 'Ajouter Membre',
subtitle: 'Nouveau membre',
description: 'Ajouter un nouveau membre à l\'organisation',
onTap: () => widget.onActionTap?.call('add_member'),
badge: '+',
),
DashboardQuickAction.success(
icon: Icons.payment,
title: 'Cotisation',
subtitle: 'Enregistrer',
description: 'Enregistrer une nouvelle cotisation',
onTap: () => widget.onActionTap?.call('add_cotisation'),
),
DashboardQuickAction(
icon: Icons.event_note,
title: 'Événement',
subtitle: 'Créer',
description: 'Créer un nouvel événement',
color: ColorTokens.tertiary,
type: QuickActionType.info,
style: QuickActionStyle.outlined,
onTap: () => widget.onActionTap?.call('create_event'),
),
DashboardQuickAction(
icon: Icons.volunteer_activism,
title: 'Solidarité',
subtitle: 'Demande',
description: 'Créer une demande de solidarité',
color: ColorTokens.warning,
type: QuickActionType.warning,
style: QuickActionStyle.outlined,
onTap: () => widget.onActionTap?.call('solidarity_request'),
secondaryIcon: Icons.favorite,
),
DashboardQuickAction(
icon: Icons.analytics,
title: 'Rapports',
subtitle: 'Générer',
description: 'Générer des rapports analytiques',
color: ColorTokens.secondary,
type: QuickActionType.secondary,
style: QuickActionStyle.minimal,
onTap: () => widget.onActionTap?.call('generate_reports'),
),
DashboardQuickAction.gradient(
icon: Icons.settings,
title: 'Paramètres',
subtitle: 'Configurer',
description: 'Accéder aux paramètres système',
gradient: const LinearGradient(
colors: [ColorTokens.primary, ColorTokens.secondary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
onTap: () => widget.onActionTap?.call('settings'),
),
];
}
@override
Widget build(BuildContext context) {
if (_filteredActions.isEmpty) {
return const SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showTitle) _buildHeader(),
if (widget.showTitle) const SizedBox(height: SpacingTokens.md),
_buildActionsLayout(),
if (widget.debugMode) _buildDebugInfo(),
],
);
}
/// Construit l'en-tête de la section
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.title ?? 'Actions rapides',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
if (widget.subtitle != null) ...[
const SizedBox(height: 4),
Text(
widget.subtitle!,
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
],
),
),
if (widget.onSeeAll != null)
TextButton(
onPressed: widget.onSeeAll,
child: const Text('Voir tout'),
),
],
);
}
/// Construit le layout des actions selon le type choisi
Widget _buildActionsLayout() {
switch (widget.layout) {
case QuickActionsLayout.grid2x2:
return _buildGridLayout(2);
case QuickActionsLayout.grid3x2:
return _buildGridLayout(3);
case QuickActionsLayout.grid4x2:
return _buildGridLayout(4);
case QuickActionsLayout.horizontal:
return _buildHorizontalLayout();
case QuickActionsLayout.vertical:
return _buildVerticalLayout();
case QuickActionsLayout.staggered:
return _buildStaggeredLayout();
case QuickActionsLayout.carousel:
return _buildCarouselLayout();
}
}
/// Construit une grille standard avec format rectangulaire compact
Widget _buildGridLayout(int crossAxisCount) {
final spacing = widget.spacing ?? SpacingTokens.sm;
// Ratio d'aspect plus rectangulaire (largeur réduite de moitié)
final aspectRatio = widget.aspectRatio ??
(widget.style == QuickActionsGridStyle.compact ? 1.8 : 1.6);
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: spacing,
mainAxisSpacing: spacing,
childAspectRatio: aspectRatio,
),
itemCount: _filteredActions.length,
itemBuilder: (context, index) {
return _buildAnimatedActionButton(index);
},
);
}
/// Construit un layout horizontal avec boutons rectangulaires compacts
Widget _buildHorizontalLayout() {
final spacing = widget.spacing ?? SpacingTokens.sm;
return SizedBox(
height: 80, // Hauteur réduite pour format rectangulaire
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _filteredActions.length,
separatorBuilder: (context, index) => SizedBox(width: spacing),
itemBuilder: (context, index) {
return SizedBox(
width: 100, // Largeur réduite de moitié (140 -> 100)
child: _buildAnimatedActionButton(index),
);
},
),
);
}
/// Construit un layout vertical
Widget _buildVerticalLayout() {
final spacing = widget.spacing ?? SpacingTokens.sm;
return Column(
children: _filteredActions.asMap().entries.map((entry) {
final index = entry.key;
return Padding(
padding: EdgeInsets.only(bottom: index < _filteredActions.length - 1 ? spacing : 0),
child: _buildAnimatedActionButton(index),
);
}).toList(),
);
}
/// Construit un layout décalé (staggered) avec format rectangulaire
Widget _buildStaggeredLayout() {
// Implémentation simplifiée du staggered layout avec dimensions réduites
return Wrap(
spacing: widget.spacing ?? SpacingTokens.sm,
runSpacing: widget.spacing ?? SpacingTokens.sm,
children: _filteredActions.asMap().entries.map((entry) {
final index = entry.key;
return SizedBox(
width: (MediaQuery.of(context).size.width - 48 - (widget.spacing ?? SpacingTokens.sm)) / 2,
height: index.isEven ? 70 : 85, // Hauteurs alternées réduites
child: _buildAnimatedActionButton(index),
);
}).toList(),
);
}
/// Construit un carrousel horizontal avec format rectangulaire compact
Widget _buildCarouselLayout() {
return SizedBox(
height: 90, // Hauteur réduite pour format rectangulaire
child: PageView.builder(
controller: PageController(viewportFraction: 0.6), // Fraction réduite pour largeur plus petite
itemCount: _filteredActions.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: widget.spacing ?? 6.0),
child: _buildAnimatedActionButton(index),
);
},
),
);
}
/// Construit un bouton d'action avec animation
Widget _buildAnimatedActionButton(int index) {
if (!widget.animated || _itemAnimations.isEmpty || index >= _itemAnimations.length) {
return DashboardQuickActionButton(action: _filteredActions[index]);
}
return AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return Transform.scale(
scale: _itemAnimations[index].value,
child: Opacity(
opacity: _itemAnimations[index].value,
child: child,
),
);
},
child: DashboardQuickActionButton(action: _filteredActions[index]),
);
}
/// Construit les informations de débogage
Widget _buildDebugInfo() {
return Container(
margin: const EdgeInsets.only(top: SpacingTokens.md),
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: ColorTokens.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ColorTokens.warning.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Debug Info:',
style: TypographyTokens.labelSmall.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.warning,
),
),
const SizedBox(height: 4),
Text(
'Layout: ${widget.layout.name}',
style: TypographyTokens.bodySmall,
),
Text(
'Style: ${widget.style.name}',
style: TypographyTokens.bodySmall,
),
Text(
'Actions: ${_filteredActions.length}',
style: TypographyTokens.bodySmall,
),
Text(
'Animated: ${widget.animated}',
style: TypographyTokens.bodySmall,
),
],
),
);
}
}

View File

@@ -1,98 +0,0 @@
/// Widget de section d'activité récente du dashboard
/// Affiche les dernières activités dans une liste compacte
library dashboard_recent_activity_section;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_activity_tile.dart';
/// Widget de section d'activité récente
///
/// Affiche les dernières activités de l'union :
/// - Nouveaux membres
/// - Cotisations reçues
/// - Événements créés
/// - Demandes de solidarité
///
/// Chaque activité peut être tapée pour plus de détails
class DashboardRecentActivitySection extends StatelessWidget {
/// Callback pour les actions sur les activités
final Function(String activityId)? onActivityTap;
/// Liste des activités à afficher
final List<DashboardActivity>? activities;
/// Constructeur de la section d'activité récente
const DashboardRecentActivitySection({
super.key,
this.onActivityTap,
this.activities,
});
/// Génère la liste des activités récentes par défaut
List<DashboardActivity> _getDefaultActivities() {
return [
DashboardActivity(
title: 'Nouveau membre ajouté',
subtitle: 'Marie Dupont a rejoint l\'union',
icon: Icons.person_add,
color: ColorTokens.primary,
time: 'Il y a 2h',
onTap: () => onActivityTap?.call('member_added_001'),
),
DashboardActivity(
title: 'Cotisation reçue',
subtitle: 'Paiement de 50€ de Jean Martin',
icon: Icons.payment,
color: ColorTokens.success,
time: 'Il y a 4h',
onTap: () => onActivityTap?.call('cotisation_002'),
),
DashboardActivity(
title: 'Événement créé',
subtitle: 'Assemblée générale programmée',
icon: Icons.event,
color: ColorTokens.tertiary,
time: 'Hier',
onTap: () => onActivityTap?.call('event_003'),
),
];
}
@override
Widget build(BuildContext context) {
final activitiesToShow = activities ?? _getDefaultActivities();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Activité récente',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm),
child: Column(
children: activitiesToShow.map((activity) {
final isLast = activity == activitiesToShow.last;
return Column(
children: [
DashboardActivityTile(activity: activity),
if (!isLast) const Divider(height: 1),
],
);
}).toList(),
),
),
),
],
);
}
}

View File

@@ -1,946 +0,0 @@
/// Widget de carte de statistique individuelle - Version Améliorée
/// Affiche une métrique sophistiquée avec animations, tendances et comparaisons
library dashboard_stats_card;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Types de statistiques disponibles
enum StatType {
count,
percentage,
currency,
duration,
rate,
score,
custom,
}
/// Styles de cartes de statistiques
enum StatCardStyle {
standard,
minimal,
elevated,
outlined,
gradient,
compact,
detailed,
}
/// Tailles de cartes de statistiques
enum StatCardSize {
small,
medium,
large,
extraLarge,
}
/// Tendances des statistiques
enum StatTrend {
up,
down,
stable,
unknown,
}
/// Modèle de données avancé pour une statistique
class DashboardStat {
/// Icône représentative de la statistique
final IconData icon;
/// Valeur numérique à afficher
final String value;
/// Titre descriptif de la statistique
final String title;
/// Sous-titre ou description
final String? subtitle;
/// Couleur thématique de la carte
final Color color;
/// Type de statistique
final StatType type;
/// Style de la carte
final StatCardStyle style;
/// Taille de la carte
final StatCardSize size;
/// Callback optionnel lors du tap sur la carte
final VoidCallback? onTap;
/// Callback optionnel lors du long press
final VoidCallback? onLongPress;
/// Valeur précédente pour comparaison
final String? previousValue;
/// Pourcentage de changement
final double? changePercentage;
/// Tendance de la statistique
final StatTrend trend;
/// Période de comparaison
final String? period;
/// Icône de tendance personnalisée
final IconData? trendIcon;
/// Gradient personnalisé
final Gradient? gradient;
/// Badge à afficher
final String? badge;
/// Couleur du badge
final Color? badgeColor;
/// Graphique miniature (sparkline)
final List<double>? sparklineData;
/// Animation activée
final bool animated;
/// Feedback haptique activé
final bool hapticFeedback;
/// Formatage personnalisé de la valeur
final String Function(String)? valueFormatter;
/// Constructeur du modèle de statistique amélioré
const DashboardStat({
required this.icon,
required this.value,
required this.title,
this.subtitle,
required this.color,
this.type = StatType.count,
this.style = StatCardStyle.standard,
this.size = StatCardSize.medium,
this.onTap,
this.onLongPress,
this.previousValue,
this.changePercentage,
this.trend = StatTrend.unknown,
this.period,
this.trendIcon,
this.gradient,
this.badge,
this.badgeColor,
this.sparklineData,
this.animated = true,
this.hapticFeedback = true,
this.valueFormatter,
});
/// Constructeur pour statistique de comptage
const DashboardStat.count({
required this.icon,
required this.value,
required this.title,
this.subtitle,
required this.color,
this.onTap,
this.onLongPress,
this.previousValue,
this.changePercentage,
this.trend = StatTrend.unknown,
this.period,
this.badge,
this.size = StatCardSize.medium,
this.animated = true,
this.hapticFeedback = true,
}) : type = StatType.count,
style = StatCardStyle.standard,
trendIcon = null,
gradient = null,
badgeColor = null,
sparklineData = null,
valueFormatter = null;
/// Constructeur pour pourcentage
const DashboardStat.percentage({
required this.icon,
required this.value,
required this.title,
this.subtitle,
required this.color,
this.onTap,
this.onLongPress,
this.changePercentage,
this.trend = StatTrend.unknown,
this.period,
this.size = StatCardSize.medium,
this.animated = true,
this.hapticFeedback = true,
}) : type = StatType.percentage,
style = StatCardStyle.elevated,
previousValue = null,
trendIcon = null,
gradient = null,
badge = null,
badgeColor = null,
sparklineData = null,
valueFormatter = null;
/// Constructeur pour devise
const DashboardStat.currency({
required this.icon,
required this.value,
required this.title,
this.subtitle,
required this.color,
this.onTap,
this.onLongPress,
this.previousValue,
this.changePercentage,
this.trend = StatTrend.unknown,
this.period,
this.sparklineData,
this.size = StatCardSize.medium,
this.animated = true,
this.hapticFeedback = true,
}) : type = StatType.currency,
style = StatCardStyle.detailed,
trendIcon = null,
gradient = null,
badge = null,
badgeColor = null,
valueFormatter = null;
/// Constructeur avec gradient
const DashboardStat.gradient({
required this.icon,
required this.value,
required this.title,
this.subtitle,
required this.gradient,
this.onTap,
this.onLongPress,
this.changePercentage,
this.trend = StatTrend.unknown,
this.period,
this.size = StatCardSize.medium,
this.animated = true,
this.hapticFeedback = true,
}) : type = StatType.custom,
style = StatCardStyle.gradient,
color = ColorTokens.primary,
previousValue = null,
trendIcon = null,
badge = null,
badgeColor = null,
sparklineData = null,
valueFormatter = null;
}
/// Widget de carte de statistique amélioré
///
/// Affiche une métrique sophistiquée avec :
/// - Icône colorée thématique avec animations
/// - Valeur numérique formatée et mise en évidence
/// - Titre et sous-titre descriptifs
/// - Indicateurs de tendance et comparaisons
/// - Graphiques miniatures (sparklines)
/// - Badges et notifications
/// - Styles multiples (standard, gradient, minimal)
/// - Design Material 3 avec élévation adaptative
/// - Support du tap et long press avec feedback haptique
class DashboardStatsCard extends StatefulWidget {
/// Données de la statistique à afficher
final DashboardStat stat;
/// Constructeur de la carte de statistique améliorée
const DashboardStatsCard({
super.key,
required this.stat,
});
@override
State<DashboardStatsCard> createState() => _DashboardStatsCardState();
}
class _DashboardStatsCardState extends State<DashboardStatsCard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
@override
void initState() {
super.initState();
_setupAnimations();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// Configure les animations
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 30.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOutCubic),
));
if (widget.stat.animated) {
_animationController.forward();
} else {
_animationController.value = 1.0;
}
}
/// Obtient les dimensions selon la taille
EdgeInsets _getPadding() {
switch (widget.stat.size) {
case StatCardSize.small:
return const EdgeInsets.all(SpacingTokens.sm);
case StatCardSize.medium:
return const EdgeInsets.all(SpacingTokens.md);
case StatCardSize.large:
return const EdgeInsets.all(SpacingTokens.lg);
case StatCardSize.extraLarge:
return const EdgeInsets.all(SpacingTokens.xl);
}
}
/// Obtient la taille de l'icône selon la taille de la carte
double _getIconSize() {
switch (widget.stat.size) {
case StatCardSize.small:
return 20.0;
case StatCardSize.medium:
return 28.0;
case StatCardSize.large:
return 36.0;
case StatCardSize.extraLarge:
return 44.0;
}
}
/// Obtient le style de texte pour la valeur
TextStyle _getValueStyle() {
final baseStyle = widget.stat.size == StatCardSize.small
? TypographyTokens.headlineSmall
: widget.stat.size == StatCardSize.medium
? TypographyTokens.headlineMedium
: widget.stat.size == StatCardSize.large
? TypographyTokens.headlineLarge
: TypographyTokens.displaySmall;
return baseStyle.copyWith(
fontWeight: FontWeight.w700,
color: _getTextColor(),
);
}
/// Obtient le style de texte pour le titre
TextStyle _getTitleStyle() {
final baseStyle = widget.stat.size == StatCardSize.small
? TypographyTokens.bodySmall
: widget.stat.size == StatCardSize.medium
? TypographyTokens.bodyMedium
: TypographyTokens.bodyLarge;
return baseStyle.copyWith(
color: _getSecondaryTextColor(),
fontWeight: FontWeight.w500,
);
}
/// Obtient la couleur du texte selon le style
Color _getTextColor() {
switch (widget.stat.style) {
case StatCardStyle.gradient:
return Colors.white;
case StatCardStyle.standard:
case StatCardStyle.minimal:
case StatCardStyle.elevated:
case StatCardStyle.outlined:
case StatCardStyle.compact:
case StatCardStyle.detailed:
return widget.stat.color;
}
}
/// Obtient la couleur du texte secondaire
Color _getSecondaryTextColor() {
switch (widget.stat.style) {
case StatCardStyle.gradient:
return Colors.white.withOpacity(0.9);
case StatCardStyle.standard:
case StatCardStyle.minimal:
case StatCardStyle.elevated:
case StatCardStyle.outlined:
case StatCardStyle.compact:
case StatCardStyle.detailed:
return ColorTokens.onSurfaceVariant;
}
}
/// Gère le tap avec feedback haptique
void _handleTap() {
if (widget.stat.hapticFeedback) {
HapticFeedback.lightImpact();
}
widget.stat.onTap?.call();
}
/// Gère le long press
void _handleLongPress() {
if (widget.stat.hapticFeedback) {
HapticFeedback.mediumImpact();
}
widget.stat.onLongPress?.call();
}
@override
Widget build(BuildContext context) {
if (!widget.stat.animated) {
return _buildCard();
}
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: Opacity(
opacity: _fadeAnimation.value,
child: child,
),
),
);
},
child: _buildCard(),
);
}
/// Construit la carte selon le style défini
Widget _buildCard() {
switch (widget.stat.style) {
case StatCardStyle.standard:
return _buildStandardCard();
case StatCardStyle.minimal:
return _buildMinimalCard();
case StatCardStyle.elevated:
return _buildElevatedCard();
case StatCardStyle.outlined:
return _buildOutlinedCard();
case StatCardStyle.gradient:
return _buildGradientCard();
case StatCardStyle.compact:
return _buildCompactCard();
case StatCardStyle.detailed:
return _buildDetailedCard();
}
}
/// Construit une carte standard
Widget _buildStandardCard() {
return Card(
elevation: 1,
child: InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: _getPadding(),
child: _buildCardContent(),
),
),
);
}
/// Construit une carte minimale
Widget _buildMinimalCard() {
return InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: _getPadding(),
decoration: BoxDecoration(
color: widget.stat.color.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.stat.color.withOpacity(0.2),
width: 1,
),
),
child: _buildCardContent(),
),
);
}
/// Construit une carte élevée
Widget _buildElevatedCard() {
return Card(
elevation: 4,
shadowColor: widget.stat.color.withOpacity(0.3),
child: InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: _getPadding(),
child: _buildCardContent(),
),
),
);
}
/// Construit une carte avec contour
Widget _buildOutlinedCard() {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: widget.stat.color,
width: 2,
),
),
child: InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: _getPadding(),
child: _buildCardContent(),
),
),
);
}
/// Construit une carte avec gradient
Widget _buildGradientCard() {
return Container(
decoration: BoxDecoration(
gradient: widget.stat.gradient ?? LinearGradient(
colors: [widget.stat.color, widget.stat.color.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: widget.stat.color.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: _getPadding(),
child: _buildCardContent(),
),
),
),
);
}
/// Construit une carte compacte
Widget _buildCompactCard() {
return Card(
elevation: 1,
child: InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.sm),
child: Row(
children: [
Icon(
widget.stat.icon,
size: 24,
color: widget.stat.color,
),
const SizedBox(width: SpacingTokens.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.stat.value,
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
color: widget.stat.color,
),
),
Text(
widget.stat.title,
style: TypographyTokens.bodySmall.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
),
),
if (widget.stat.trend != StatTrend.unknown)
_buildTrendIndicator(),
],
),
),
),
);
}
/// Construit une carte détaillée
Widget _buildDetailedCard() {
return Card(
elevation: 2,
child: InkWell(
onTap: _handleTap,
onLongPress: _handleLongPress,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: _getPadding(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
widget.stat.icon,
size: _getIconSize(),
color: widget.stat.color,
),
if (widget.stat.badge != null) _buildBadge(),
],
),
const SizedBox(height: SpacingTokens.md),
Text(
_formatValue(widget.stat.value),
style: _getValueStyle(),
),
const SizedBox(height: SpacingTokens.xs),
Text(
widget.stat.title,
style: _getTitleStyle(),
),
if (widget.stat.subtitle != null) ...[
const SizedBox(height: 2),
Text(
widget.stat.subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: _getSecondaryTextColor().withOpacity(0.7),
),
),
],
if (widget.stat.changePercentage != null) ...[
const SizedBox(height: SpacingTokens.sm),
_buildChangeIndicator(),
],
if (widget.stat.sparklineData != null) ...[
const SizedBox(height: SpacingTokens.sm),
_buildSparkline(),
],
],
),
),
),
);
}
/// Construit le contenu standard de la carte
Widget _buildCardContent() {
return Stack(
clipBehavior: Clip.none,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.stat.icon,
size: _getIconSize(),
color: _getTextColor(),
),
const SizedBox(height: SpacingTokens.sm),
Text(
_formatValue(widget.stat.value),
style: _getValueStyle(),
textAlign: TextAlign.center,
),
const SizedBox(height: SpacingTokens.xs),
Text(
widget.stat.title,
style: _getTitleStyle(),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (widget.stat.subtitle != null) ...[
const SizedBox(height: 2),
Text(
widget.stat.subtitle!,
style: TypographyTokens.bodySmall.copyWith(
color: _getSecondaryTextColor().withOpacity(0.7),
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
if (widget.stat.changePercentage != null) ...[
const SizedBox(height: SpacingTokens.xs),
_buildChangeIndicator(),
],
],
),
// Badge en haut à droite
if (widget.stat.badge != null)
Positioned(
top: -8,
right: -8,
child: _buildBadge(),
),
],
);
}
/// Formate la valeur selon le type
String _formatValue(String value) {
if (widget.stat.valueFormatter != null) {
return widget.stat.valueFormatter!(value);
}
switch (widget.stat.type) {
case StatType.percentage:
return '$value%';
case StatType.currency:
return '$value';
case StatType.duration:
return '${value}h';
case StatType.rate:
return '$value/min';
case StatType.count:
case StatType.score:
case StatType.custom:
return value;
}
}
/// Construit l'indicateur de changement
Widget _buildChangeIndicator() {
if (widget.stat.changePercentage == null) {
return const SizedBox.shrink();
}
final isPositive = widget.stat.changePercentage! > 0;
final color = isPositive ? ColorTokens.success : ColorTokens.error;
final icon = isPositive ? Icons.trending_up : Icons.trending_down;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
widget.stat.trendIcon ?? icon,
size: 14,
color: color,
),
const SizedBox(width: 4),
Text(
'${isPositive ? '+' : ''}${widget.stat.changePercentage!.toStringAsFixed(1)}%',
style: TypographyTokens.bodySmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
if (widget.stat.period != null) ...[
const SizedBox(width: 4),
Text(
widget.stat.period!,
style: TypographyTokens.bodySmall.copyWith(
color: _getSecondaryTextColor().withOpacity(0.6),
),
),
],
],
);
}
/// Construit l'indicateur de tendance
Widget _buildTrendIndicator() {
IconData icon;
Color color;
switch (widget.stat.trend) {
case StatTrend.up:
icon = Icons.trending_up;
color = ColorTokens.success;
break;
case StatTrend.down:
icon = Icons.trending_down;
color = ColorTokens.error;
break;
case StatTrend.stable:
icon = Icons.trending_flat;
color = ColorTokens.warning;
break;
case StatTrend.unknown:
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
widget.stat.trendIcon ?? icon,
size: 16,
color: color,
),
);
}
/// Construit le badge
Widget _buildBadge() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: widget.stat.badgeColor ?? ColorTokens.error,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Text(
widget.stat.badge!,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
);
}
/// Construit un graphique miniature (sparkline)
Widget _buildSparkline() {
if (widget.stat.sparklineData == null || widget.stat.sparklineData!.isEmpty) {
return const SizedBox.shrink();
}
return SizedBox(
height: 40,
child: CustomPaint(
painter: SparklinePainter(
data: widget.stat.sparklineData!,
color: widget.stat.color,
),
),
);
}
}
/// Painter pour dessiner un graphique miniature
class SparklinePainter extends CustomPainter {
final List<double> data;
final Color color;
SparklinePainter({
required this.data,
required this.color,
});
@override
void paint(Canvas canvas, Size size) {
if (data.length < 2) return;
final paint = Paint()
..color = color
..strokeWidth = 2
..style = PaintingStyle.stroke;
final path = Path();
final maxValue = data.reduce((a, b) => a > b ? a : b);
final minValue = data.reduce((a, b) => a < b ? a : b);
final range = maxValue - minValue;
if (range == 0) return;
for (int i = 0; i < data.length; i++) {
final x = (i / (data.length - 1)) * size.width;
final y = size.height - ((data[i] - minValue) / range) * size.height;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
// Dessiner des points aux extrémités
final pointPaint = Paint()
..color = color
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(0, size.height - ((data.first - minValue) / range) * size.height),
2,
pointPaint,
);
canvas.drawCircle(
Offset(size.width, size.height - ((data.last - minValue) / range) * size.height),
2,
pointPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@@ -1,99 +0,0 @@
/// Widget de grille de statistiques du dashboard
/// Affiche les métriques principales dans une grille responsive
library dashboard_stats_grid;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_stats_card.dart';
/// Widget de grille de statistiques
///
/// Affiche les statistiques principales dans une grille 2x2 :
/// - Membres actifs
/// - Cotisations du mois
/// - Événements programmés
/// - Demandes de solidarité
///
/// Chaque carte est interactive et peut déclencher une navigation
class DashboardStatsGrid extends StatelessWidget {
/// Callback pour les actions sur les statistiques
final Function(String statType)? onStatTap;
/// Liste des statistiques à afficher
final List<DashboardStat>? stats;
/// Constructeur de la grille de statistiques
const DashboardStatsGrid({
super.key,
this.onStatTap,
this.stats,
});
/// Génère la liste des statistiques par défaut
List<DashboardStat> _getDefaultStats() {
return [
DashboardStat(
icon: Icons.people,
value: '25',
title: 'Membres',
color: ColorTokens.primary,
onTap: () => onStatTap?.call('members'),
),
DashboardStat(
icon: Icons.account_balance_wallet,
value: '15',
title: 'Cotisations',
color: ColorTokens.success,
onTap: () => onStatTap?.call('cotisations'),
),
DashboardStat(
icon: Icons.event,
value: '8',
title: 'Événements',
color: ColorTokens.tertiary,
onTap: () => onStatTap?.call('events'),
),
DashboardStat(
icon: Icons.favorite,
value: '3',
title: 'Solidarité',
color: ColorTokens.error,
onTap: () => onStatTap?.call('solidarity'),
),
];
}
@override
Widget build(BuildContext context) {
final statsToShow = stats ?? _getDefaultStats();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statistiques',
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: SpacingTokens.md),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
childAspectRatio: 1.4,
),
itemCount: statsToShow.length,
itemBuilder: (context, index) {
return DashboardStatsCard(stat: statsToShow[index]);
},
),
],
);
}
}

View File

@@ -1,70 +0,0 @@
/// Widget de section de bienvenue du dashboard
/// Affiche un message d'accueil avec gradient et design moderne
library dashboard_welcome_section;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
/// Widget de section de bienvenue
///
/// Affiche un message d'accueil personnalisé avec :
/// - Gradient de fond élégant
/// - Typographie hiérarchisée
/// - Design responsive et moderne
class DashboardWelcomeSection extends StatelessWidget {
/// Titre principal de la section
final String title;
/// Sous-titre descriptif
final String subtitle;
/// Constructeur du widget de bienvenue
const DashboardWelcomeSection({
super.key,
this.title = 'Bienvenue sur UnionFlow',
this.subtitle = 'Votre plateforme de gestion d\'union familiale',
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(SpacingTokens.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ColorTokens.primary.withOpacity(0.1),
ColorTokens.secondary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(SpacingTokens.radiusMd),
border: Border.all(
color: ColorTokens.outline.withOpacity(0.1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.w700,
color: ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
subtitle,
style: TypographyTokens.bodyMedium.copyWith(
color: ColorTokens.onSurfaceVariant,
),
),
],
),
);
}
}

View File

@@ -1,191 +1,12 @@
library dashboard_widgets;
/// Exports pour tous les widgets du dashboard UnionFlow
///
/// Ce fichier centralise tous les imports des composants du dashboard
/// pour faciliter leur utilisation dans les pages et autres widgets.
// Widgets communs réutilisables
export 'common/stat_card.dart';
export 'common/section_header.dart';
export 'common/activity_item.dart';
// Sections principales du dashboard
export 'dashboard_header.dart';
export 'quick_stats_section.dart';
export 'recent_activities_section.dart';
export 'upcoming_events_section.dart';
// Composants spécialisés
export 'components/cards/performance_card.dart';
// Widgets existants (legacy) - gardés pour compatibilité
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/tokens.dart';
import '../../../../shared/design_system/dashboard_theme.dart';
/// Widget pour afficher une grille d'actions rapides
class DashboardQuickActionsGrid extends StatelessWidget {
final List<Widget> children;
final int crossAxisCount;
const DashboardQuickActionsGrid({
super.key,
required this.children,
this.crossAxisCount = 2,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
childAspectRatio: 1.2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
children: children,
);
}
}
/// Widget pour une action rapide
class DashboardQuickAction extends StatelessWidget {
final String title;
final IconData icon;
final Color? color;
final VoidCallback? onTap;
const DashboardQuickAction({
super.key,
required this.title,
required this.icon,
this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RadiusTokens.md),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 32,
color: color ?? ColorTokens.primary,
),
const SizedBox(height: SpacingTokens.sm),
Text(
title,
style: TypographyTokens.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}
/// Widget pour afficher une section d'activité récente
class DashboardRecentActivitySection extends StatelessWidget {
final List<Widget> children;
final String title;
const DashboardRecentActivitySection({
super.key,
required this.children,
this.title = 'Activité Récente',
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TypographyTokens.headlineSmall,
),
const SizedBox(height: SpacingTokens.md),
...children,
],
);
}
}
/// Widget pour une activité
class DashboardActivity extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Color? color;
const DashboardActivity({
super.key,
required this.title,
required this.subtitle,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: SpacingTokens.sm),
child: ListTile(
leading: CircleAvatar(
backgroundColor: color ?? ColorTokens.primary,
child: Icon(icon, color: Colors.white),
),
title: Text(title),
subtitle: Text(subtitle),
),
);
}
}
/// Widget pour une section d'insights
class DashboardInsightsSection extends StatelessWidget {
final List<Widget> children;
const DashboardInsightsSection({
super.key,
required this.children,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Insights',
style: TypographyTokens.headlineSmall,
),
const SizedBox(height: SpacingTokens.md),
...children,
],
);
}
}
/// Widget pour une statistique
/// 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;
final VoidCallback? onTap;
const DashboardStat({
super.key,
@@ -193,59 +14,56 @@ class DashboardStat extends StatelessWidget {
required this.value,
required this.icon,
this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RadiusTokens.md),
child: Padding(
padding: const EdgeInsets.all(SpacingTokens.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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,
size: 32,
color: color ?? ColorTokens.primary,
color: color ?? DashboardTheme.royalBlue,
size: 24,
),
const SizedBox(height: SpacingTokens.sm),
const Spacer(),
Text(
value,
style: TypographyTokens.headlineSmall.copyWith(
fontWeight: FontWeight.bold,
color: color ?? ColorTokens.primary,
style: DashboardTheme.titleLarge.copyWith(
color: color ?? DashboardTheme.royalBlue,
),
),
const SizedBox(height: SpacingTokens.xs),
Text(
title,
style: TypographyTokens.bodySmall,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(height: DashboardTheme.spacing8),
Text(
title,
style: DashboardTheme.bodyMedium,
),
],
),
);
}
}
/// Widget pour la grille de statistiques
/// Widget de grille de statistiques
class DashboardStatsGrid extends StatelessWidget {
final List<Widget> children;
final int crossAxisCount;
final List<DashboardStat> stats;
final Function(String)? onStatTap;
const DashboardStatsGrid({
super.key,
required this.children,
this.crossAxisCount = 2,
required this.stats,
this.onStatTap,
});
@override
@@ -253,64 +71,182 @@ class DashboardStatsGrid extends StatelessWidget {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
crossAxisCount: 2,
mainAxisSpacing: DashboardTheme.spacing12,
crossAxisSpacing: DashboardTheme.spacing12,
childAspectRatio: 1.2,
crossAxisSpacing: SpacingTokens.md,
mainAxisSpacing: SpacingTokens.md,
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 pour le drawer du dashboard
class DashboardDrawer extends StatelessWidget {
const DashboardDrawer({super.key});
/// 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 Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(
color: ColorTokens.primary,
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,
),
child: Text(
'UnionFlow',
style: TextStyle(
color: Colors.white,
fontSize: 24,
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,
),
),
ListTile(
leading: const Icon(Icons.dashboard),
title: const Text('Dashboard'),
onTap: () {
Navigator.pop(context);
},
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,
),
],
),
),
ListTile(
leading: const Icon(Icons.people),
title: const Text('Membres'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.event),
title: const Text('Événements'),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Paramètres'),
onTap: () {
Navigator.pop(context);
},
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);
// TODO: Implémenter l'action
},
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

@@ -1,359 +0,0 @@
import 'package:flutter/material.dart';
import 'common/section_header.dart';
import 'common/stat_card.dart';
/// Section des statistiques rapides du dashboard
///
/// Widget réutilisable pour afficher les KPIs et métriques principales
/// avec différents layouts et styles selon le contexte.
class QuickStatsSection extends StatelessWidget {
/// Titre de la section
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Liste des statistiques à afficher
final List<QuickStat> stats;
/// Layout des cartes (grid, row, column)
final StatsLayout layout;
/// Nombre de colonnes pour le layout grid
final int gridColumns;
/// Style des cartes de statistiques
final StatCardStyle cardStyle;
/// Taille des cartes
final StatCardSize cardSize;
/// Callback lors du tap sur une statistique
final Function(QuickStat)? onStatTap;
/// Afficher ou non l'en-tête de section
final bool showHeader;
const QuickStatsSection({
super.key,
required this.title,
this.subtitle,
required this.stats,
this.layout = StatsLayout.grid,
this.gridColumns = 2,
this.cardStyle = StatCardStyle.elevated,
this.cardSize = StatCardSize.compact,
this.onStatTap,
this.showHeader = true,
});
/// Constructeur pour les KPIs système (Super Admin)
const QuickStatsSection.systemKPIs({
super.key,
this.onStatTap,
}) : title = 'Métriques Système',
subtitle = null,
stats = const [
QuickStat(
title: 'Organisations',
value: '247',
subtitle: '+12 ce mois',
icon: Icons.business,
color: Color(0xFF0984E3),
),
QuickStat(
title: 'Utilisateurs',
value: '15,847',
subtitle: '+1,234 ce mois',
icon: Icons.people,
color: Color(0xFF00B894),
),
QuickStat(
title: 'Uptime',
value: '99.97%',
subtitle: '30 derniers jours',
icon: Icons.trending_up,
color: Color(0xFF00CEC9),
),
QuickStat(
title: 'Temps Réponse',
value: '1.2s',
subtitle: 'Moyenne 24h',
icon: Icons.speed,
color: Color(0xFFE17055),
),
],
layout = StatsLayout.grid,
gridColumns = 2,
cardStyle = StatCardStyle.elevated,
cardSize = StatCardSize.compact,
showHeader = true;
/// Constructeur pour les statistiques d'organisation
const QuickStatsSection.organizationStats({
super.key,
this.onStatTap,
}) : title = 'Vue d\'ensemble',
subtitle = null,
stats = const [
QuickStat(
title: 'Membres',
value: '156',
subtitle: '+12 ce mois',
icon: Icons.people,
color: Color(0xFF00B894),
),
QuickStat(
title: 'Événements',
value: '23',
subtitle: '8 à venir',
icon: Icons.event,
color: Color(0xFFE17055),
),
QuickStat(
title: 'Projets',
value: '8',
subtitle: '3 actifs',
icon: Icons.work,
color: Color(0xFF0984E3),
),
QuickStat(
title: 'Taux engagement',
value: '78%',
subtitle: '+5% ce mois',
icon: Icons.trending_up,
color: Color(0xFF6C5CE7),
),
],
layout = StatsLayout.grid,
gridColumns = 2,
cardStyle = StatCardStyle.elevated,
cardSize = StatCardSize.compact,
showHeader = true;
/// Constructeur pour les métriques de performance
const QuickStatsSection.performanceMetrics({
super.key,
this.onStatTap,
}) : title = 'Performance',
subtitle = 'Métriques temps réel',
stats = const [
QuickStat(
title: 'CPU',
value: '23%',
subtitle: 'Normal',
icon: Icons.memory,
color: Color(0xFF00B894),
),
QuickStat(
title: 'RAM',
value: '67%',
subtitle: 'Élevé',
icon: Icons.storage,
color: Color(0xFFE17055),
),
QuickStat(
title: 'Réseau',
value: '12 MB/s',
subtitle: 'Stable',
icon: Icons.network_check,
color: Color(0xFF0984E3),
),
],
layout = StatsLayout.row,
gridColumns = 3,
cardStyle = StatCardStyle.outlined,
cardSize = StatCardSize.normal,
showHeader = true;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showHeader) ...[
SectionHeader.section(
title: title,
subtitle: subtitle,
),
],
_buildStatsLayout(),
],
);
}
/// Construction du layout des statistiques
Widget _buildStatsLayout() {
switch (layout) {
case StatsLayout.grid:
return _buildGridLayout();
case StatsLayout.row:
return _buildRowLayout();
case StatsLayout.column:
return _buildColumnLayout();
case StatsLayout.wrap:
return _buildWrapLayout();
}
}
/// Layout en grille
Widget _buildGridLayout() {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: gridColumns,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: _getChildAspectRatio(),
),
itemCount: stats.length,
itemBuilder: (context, index) => _buildStatCard(stats[index]),
);
}
/// Layout en ligne
Widget _buildRowLayout() {
return Row(
children: stats.map((stat) => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: _buildStatCard(stat),
),
)).toList(),
);
}
/// Layout en colonne
Widget _buildColumnLayout() {
return Column(
children: stats.map((stat) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildStatCard(stat),
)).toList(),
);
}
/// Layout wrap (adaptatif)
Widget _buildWrapLayout() {
return LayoutBuilder(
builder: (context, constraints) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: stats.map((stat) => SizedBox(
width: (constraints.maxWidth - 8) / 2, // 2 colonnes avec espacement
child: _buildStatCard(stat),
)).toList(),
);
},
);
}
/// Construction d'une carte de statistique
Widget _buildStatCard(QuickStat stat) {
return StatCard(
title: stat.title,
value: stat.value,
subtitle: stat.subtitle,
icon: stat.icon,
color: stat.color,
size: cardSize,
style: cardStyle,
onTap: onStatTap != null ? () => onStatTap!(stat) : null,
);
}
/// Ratio d'aspect selon la taille des cartes
double _getChildAspectRatio() {
switch (cardSize) {
case StatCardSize.compact:
return 1.4;
case StatCardSize.normal:
return 1.2;
case StatCardSize.large:
return 1.0;
}
}
}
/// Modèle de données pour une statistique rapide
class QuickStat {
final String title;
final String value;
final String subtitle;
final IconData icon;
final Color color;
final Map<String, dynamic>? metadata;
const QuickStat({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
required this.color,
this.metadata,
});
/// Constructeur pour une métrique système
const QuickStat.system({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
}) : color = const Color(0xFF6C5CE7),
metadata = null;
/// Constructeur pour une métrique utilisateur
const QuickStat.user({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
}) : color = const Color(0xFF00B894),
metadata = null;
/// Constructeur pour une métrique d'organisation
const QuickStat.organization({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
}) : color = const Color(0xFF0984E3),
metadata = null;
/// Constructeur pour une métrique d'événement
const QuickStat.event({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
}) : color = const Color(0xFFE17055),
metadata = null;
/// Constructeur pour une alerte
const QuickStat.alert({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
}) : color = Colors.orange,
metadata = null;
/// Constructeur pour une erreur
const QuickStat.error({
required this.title,
required this.value,
required this.subtitle,
required this.icon,
}) : color = Colors.red,
metadata = null;
}
/// Types de layout pour les statistiques
enum StatsLayout {
grid,
row,
column,
wrap,
}

View File

@@ -1,366 +0,0 @@
import 'package:flutter/material.dart';
import 'common/activity_item.dart';
/// Section des activités récentes du dashboard
///
/// Widget réutilisable pour afficher les dernières activités,
/// notifications, logs ou événements selon le contexte.
class RecentActivitiesSection extends StatelessWidget {
/// Titre de la section
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Liste des activités à afficher
final List<RecentActivity> activities;
/// Nombre maximum d'activités à afficher
final int maxItems;
/// Style des éléments d'activité
final ActivityItemStyle itemStyle;
/// Callback lors du tap sur une activité
final Function(RecentActivity)? onActivityTap;
/// Callback pour voir toutes les activités
final VoidCallback? onViewAll;
/// Afficher ou non l'en-tête de section
final bool showHeader;
/// Afficher ou non le bouton "Voir tout"
final bool showViewAll;
/// Message à afficher si aucune activité
final String? emptyMessage;
const RecentActivitiesSection({
super.key,
required this.title,
this.subtitle,
required this.activities,
this.maxItems = 5,
this.itemStyle = ActivityItemStyle.normal,
this.onActivityTap,
this.onViewAll,
this.showHeader = true,
this.showViewAll = true,
this.emptyMessage,
});
/// Constructeur pour les activités système (Super Admin)
const RecentActivitiesSection.system({
super.key,
this.onActivityTap,
this.onViewAll,
}) : title = 'Activité Système',
subtitle = 'Événements récents',
activities = const [
RecentActivity(
title: 'Sauvegarde automatique terminée',
description: 'Sauvegarde complète réussie (2.3 GB)',
timestamp: 'il y a 1h',
type: ActivityType.system,
),
RecentActivity(
title: 'Nouvelle organisation créée',
description: 'TechCorp a rejoint la plateforme',
timestamp: 'il y a 2h',
type: ActivityType.organization,
),
RecentActivity(
title: 'Mise à jour système',
description: 'Version 2.1.0 déployée avec succès',
timestamp: 'il y a 4h',
type: ActivityType.system,
),
RecentActivity(
title: 'Alerte CPU résolue',
description: 'Charge CPU revenue à la normale',
timestamp: 'il y a 6h',
type: ActivityType.success,
),
],
maxItems = 4,
itemStyle = ActivityItemStyle.normal,
showHeader = true,
showViewAll = true,
emptyMessage = null;
/// Constructeur pour les activités d'organisation
const RecentActivitiesSection.organization({
super.key,
this.onActivityTap,
this.onViewAll,
}) : title = 'Activité Récente',
subtitle = null,
activities = const [
RecentActivity(
title: 'Nouveau membre inscrit',
description: 'Marie Dubois a rejoint l\'organisation',
timestamp: 'il y a 30min',
type: ActivityType.user,
),
RecentActivity(
title: 'Événement créé',
description: 'Réunion mensuelle programmée',
timestamp: 'il y a 2h',
type: ActivityType.event,
),
RecentActivity(
title: 'Document partagé',
description: 'Rapport Q4 2024 publié',
timestamp: 'il y a 1j',
type: ActivityType.organization,
),
],
maxItems = 3,
itemStyle = ActivityItemStyle.normal,
showHeader = true,
showViewAll = true,
emptyMessage = null;
/// Constructeur pour les alertes système
const RecentActivitiesSection.alerts({
super.key,
this.onActivityTap,
this.onViewAll,
}) : title = 'Alertes Récentes',
subtitle = 'Notifications importantes',
activities = const [
RecentActivity(
title: 'Charge CPU élevée',
description: 'Serveur principal à 85%',
timestamp: 'il y a 15min',
type: ActivityType.alert,
),
RecentActivity(
title: 'Espace disque faible',
description: 'Base de données à 90%',
timestamp: 'il y a 1h',
type: ActivityType.error,
),
RecentActivity(
title: 'Connexions élevées',
description: 'Load balancer surchargé',
timestamp: 'il y a 2h',
type: ActivityType.alert,
),
],
maxItems = 3,
itemStyle = ActivityItemStyle.alert,
showHeader = true,
showViewAll = true,
emptyMessage = null;
@override
Widget build(BuildContext context) {
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showHeader) _buildHeader(),
const SizedBox(height: 12),
_buildActivitiesList(),
],
),
);
}
/// En-tête de la section
Widget _buildHeader() {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
if (showViewAll && onViewAll != null)
TextButton(
onPressed: onViewAll,
child: const Text(
'Voir tout',
style: TextStyle(
fontSize: 12,
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.w600,
),
),
),
],
);
}
/// Liste des activités
Widget _buildActivitiesList() {
if (activities.isEmpty) {
return _buildEmptyState();
}
final displayedActivities = activities.take(maxItems).toList();
return Column(
children: displayedActivities.map((activity) => ActivityItem(
title: activity.title,
description: activity.description,
timestamp: activity.timestamp,
icon: activity.icon,
color: activity.color,
type: activity.type,
style: itemStyle,
onTap: onActivityTap != null ? () => onActivityTap!(activity) : null,
)).toList(),
);
}
/// État vide
Widget _buildEmptyState() {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Icon(
Icons.inbox_outlined,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 12),
Text(
emptyMessage ?? 'Aucune activité récente',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Modèle de données pour une activité récente
class RecentActivity {
final String title;
final String? description;
final String timestamp;
final IconData? icon;
final Color? color;
final ActivityType? type;
final Map<String, dynamic>? metadata;
const RecentActivity({
required this.title,
this.description,
required this.timestamp,
this.icon,
this.color,
this.type,
this.metadata,
});
/// Constructeur pour une activité système
const RecentActivity.system({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.settings,
color = const Color(0xFF6C5CE7),
type = ActivityType.system;
/// Constructeur pour une activité utilisateur
const RecentActivity.user({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.person,
color = const Color(0xFF00B894),
type = ActivityType.user;
/// Constructeur pour une activité d'organisation
const RecentActivity.organization({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.business,
color = const Color(0xFF0984E3),
type = ActivityType.organization;
/// Constructeur pour une activité d'événement
const RecentActivity.event({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.event,
color = const Color(0xFFE17055),
type = ActivityType.event;
/// Constructeur pour une alerte
const RecentActivity.alert({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.warning,
color = Colors.orange,
type = ActivityType.alert;
/// Constructeur pour une erreur
const RecentActivity.error({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.error,
color = Colors.red,
type = ActivityType.error;
/// Constructeur pour un succès
const RecentActivity.success({
required this.title,
this.description,
required this.timestamp,
this.metadata,
}) : icon = Icons.check_circle,
color = const Color(0xFF00B894),
type = ActivityType.success;
}

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,228 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme.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();
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: () {
// TODO: Personnaliser les raccourcis
},
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() {
return [
DashboardShortcut(
title: 'Nouveau\nMembre',
icon: Icons.person_add,
color: DashboardTheme.success,
onTap: () {
// TODO: Naviguer vers ajout membre
},
),
DashboardShortcut(
title: 'Créer\nÉvénement',
icon: Icons.event_available,
color: DashboardTheme.royalBlue,
onTap: () {
// TODO: Naviguer vers création événement
},
),
DashboardShortcut(
title: 'Ajouter\nContribution',
icon: Icons.payment,
color: DashboardTheme.tealBlue,
onTap: () {
// TODO: Naviguer vers ajout contribution
},
),
DashboardShortcut(
title: 'Envoyer\nMessage',
icon: Icons.message,
color: DashboardTheme.warning,
badge: '3',
badgeColor: DashboardTheme.error,
onTap: () {
// TODO: Naviguer vers messagerie
},
),
DashboardShortcut(
title: 'Générer\nRapport',
icon: Icons.assessment,
color: DashboardTheme.info,
onTap: () {
// TODO: Naviguer vers génération rapport
},
),
DashboardShortcut(
title: 'Paramètres',
icon: Icons.settings,
color: DashboardTheme.grey600,
onTap: () {
// TODO: Naviguer vers paramètres
},
),
];
}
}
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

@@ -1,270 +0,0 @@
/// Test rapide pour vérifier les boutons rectangulaires compacts
/// Démontre les nouvelles dimensions et le format rectangulaire
library test_rectangular_buttons;
import 'package:flutter/material.dart';
import '../../../../core/design_system/tokens/color_tokens.dart';
import '../../../../core/design_system/tokens/spacing_tokens.dart';
import '../../../../core/design_system/tokens/typography_tokens.dart';
import 'dashboard_quick_action_button.dart';
import 'dashboard_quick_actions_grid.dart';
/// Page de test pour les boutons rectangulaires
class TestRectangularButtonsPage extends StatelessWidget {
const TestRectangularButtonsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Boutons Rectangulaires - Test'),
backgroundColor: ColorTokens.primary,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(SpacingTokens.md),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('🔲 Boutons Rectangulaires Compacts'),
const SizedBox(height: SpacingTokens.md),
_buildIndividualButtons(),
const SizedBox(height: SpacingTokens.xl),
_buildSectionTitle('📊 Grilles avec Format Rectangulaire'),
const SizedBox(height: SpacingTokens.md),
_buildGridLayouts(),
const SizedBox(height: SpacingTokens.xl),
_buildSectionTitle('📏 Comparaison des Dimensions'),
const SizedBox(height: SpacingTokens.md),
_buildDimensionComparison(),
],
),
),
);
}
/// Construit un titre de section
Widget _buildSectionTitle(String title) {
return Text(
title,
style: TypographyTokens.headlineMedium.copyWith(
fontWeight: FontWeight.w700,
color: ColorTokens.primary,
),
);
}
/// Test des boutons individuels
Widget _buildIndividualButtons() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Boutons Individuels - Largeur Réduite de Moitié',
style: TypographyTokens.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.md),
// Ligne de boutons rectangulaires
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: 100, // Largeur réduite
height: 70, // Hauteur rectangulaire
child: DashboardQuickActionButton(
action: DashboardQuickAction.primary(
icon: Icons.add,
title: 'Ajouter',
subtitle: 'Nouveau',
onTap: () => _showMessage('Bouton Ajouter'),
),
),
),
SizedBox(
width: 100,
height: 70,
child: DashboardQuickActionButton(
action: DashboardQuickAction.success(
icon: Icons.check,
title: 'Valider',
subtitle: 'OK',
onTap: () => _showMessage('Bouton Valider'),
),
),
),
SizedBox(
width: 100,
height: 70,
child: DashboardQuickActionButton(
action: DashboardQuickAction.warning(
icon: Icons.warning,
title: 'Alerte',
subtitle: 'Urgent',
onTap: () => _showMessage('Bouton Alerte'),
),
),
),
],
),
],
);
}
/// Test des grilles avec différents layouts
Widget _buildGridLayouts() {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Grille compacte 2x2
DashboardQuickActionsGrid.compact(
title: 'Grille Compacte 2x2 - Format Rectangulaire',
),
SizedBox(height: SpacingTokens.xl),
// Grille étendue 3x2
DashboardQuickActionsGrid.expanded(
title: 'Grille Étendue 3x2 - Boutons Plus Petits',
subtitle: 'Ratio d\'aspect 1.5 au lieu de 2.0',
),
SizedBox(height: SpacingTokens.xl),
// Carrousel horizontal
DashboardQuickActionsGrid.carousel(
title: 'Carrousel - Hauteur Réduite (90px)',
),
],
);
}
/// Comparaison visuelle des dimensions
Widget _buildDimensionComparison() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comparaison Avant/Après',
style: TypographyTokens.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.md),
// Simulation ancien format (plus large)
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: ColorTokens.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ColorTokens.error.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'❌ AVANT - Trop Large (140x100)',
style: TypographyTokens.labelMedium.copyWith(
color: ColorTokens.error,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.sm),
Container(
width: 140,
height: 100,
decoration: BoxDecoration(
color: ColorTokens.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: ColorTokens.primary.withOpacity(0.3)),
),
child: const Center(
child: Text('Ancien Format\n140x100'),
),
),
],
),
),
const SizedBox(height: SpacingTokens.md),
// Nouveau format (rectangulaire compact)
Container(
padding: const EdgeInsets.all(SpacingTokens.sm),
decoration: BoxDecoration(
color: ColorTokens.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ColorTokens.success.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'✅ APRÈS - Rectangulaire Compact (100x70)',
style: TypographyTokens.labelMedium.copyWith(
color: ColorTokens.success,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: SpacingTokens.sm),
SizedBox(
width: 100,
height: 70,
child: DashboardQuickActionButton(
action: DashboardQuickAction.success(
icon: Icons.thumb_up,
title: 'Nouveau',
subtitle: '100x70',
onTap: () => _showMessage('Nouveau Format!'),
),
),
),
],
),
),
const SizedBox(height: SpacingTokens.md),
// Résumé des améliorations
Container(
padding: const EdgeInsets.all(SpacingTokens.md),
decoration: BoxDecoration(
color: ColorTokens.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'📊 Améliorations Apportées',
style: TypographyTokens.titleSmall.copyWith(
fontWeight: FontWeight.w600,
color: ColorTokens.primary,
),
),
const SizedBox(height: SpacingTokens.sm),
const Text('• Largeur réduite de 50% (140px → 100px)'),
const Text('• Hauteur optimisée (100px → 70px)'),
const Text('• Format rectangulaire plus compact'),
const Text('• Bordures moins arrondies (12px → 6px)'),
const Text('• Espacement réduit entre éléments'),
const Text('• Ratio d\'aspect optimisé (2.2 → 1.6)'),
],
),
),
],
);
}
/// Affiche un message de test
void _showMessage(String message) {
// Note: Cette méthode nécessiterait un BuildContext pour afficher un SnackBar
// Dans un vrai contexte, on utiliserait ScaffoldMessenger
debugPrint('Test: $message');
}
}

View File

@@ -1,473 +0,0 @@
import 'package:flutter/material.dart';
/// Section des événements à venir du dashboard
///
/// Widget réutilisable pour afficher les prochains événements,
/// réunions, échéances ou tâches selon le contexte.
class UpcomingEventsSection extends StatelessWidget {
/// Titre de la section
final String title;
/// Sous-titre optionnel
final String? subtitle;
/// Liste des événements à afficher
final List<UpcomingEvent> events;
/// Nombre maximum d'événements à afficher
final int maxItems;
/// Callback lors du tap sur un événement
final Function(UpcomingEvent)? onEventTap;
/// Callback pour voir tous les événements
final VoidCallback? onViewAll;
/// Afficher ou non l'en-tête de section
final bool showHeader;
/// Afficher ou non le bouton "Voir tout"
final bool showViewAll;
/// Message à afficher si aucun événement
final String? emptyMessage;
/// Style de la section
final EventsSectionStyle style;
const UpcomingEventsSection({
super.key,
required this.title,
this.subtitle,
required this.events,
this.maxItems = 3,
this.onEventTap,
this.onViewAll,
this.showHeader = true,
this.showViewAll = true,
this.emptyMessage,
this.style = EventsSectionStyle.card,
});
/// Constructeur pour les événements d'organisation
const UpcomingEventsSection.organization({
super.key,
this.onEventTap,
this.onViewAll,
}) : title = 'Événements à venir',
subtitle = 'Prochaines échéances',
events = const [
UpcomingEvent(
title: 'Réunion mensuelle',
description: 'Point équipe et objectifs',
date: '15 Jan 2025',
time: '14:00',
location: 'Salle de conférence',
type: EventType.meeting,
),
UpcomingEvent(
title: 'Formation sécurité',
description: 'Session obligatoire',
date: '18 Jan 2025',
time: '09:00',
location: 'En ligne',
type: EventType.training,
),
UpcomingEvent(
title: 'Assemblée générale',
description: 'Vote budget 2025',
date: '25 Jan 2025',
time: '10:00',
location: 'Auditorium',
type: EventType.assembly,
),
],
maxItems = 3,
showHeader = true,
showViewAll = true,
emptyMessage = null,
style = EventsSectionStyle.card;
/// Constructeur pour les tâches système
const UpcomingEventsSection.systemTasks({
super.key,
this.onEventTap,
this.onViewAll,
}) : title = 'Tâches Programmées',
subtitle = 'Maintenance et sauvegardes',
events = const [
UpcomingEvent(
title: 'Sauvegarde hebdomadaire',
description: 'Sauvegarde complète BDD',
date: 'Aujourd\'hui',
time: '02:00',
location: 'Automatique',
type: EventType.maintenance,
),
UpcomingEvent(
title: 'Mise à jour sécurité',
description: 'Patches système',
date: 'Demain',
time: '01:00',
location: 'Serveurs',
type: EventType.maintenance,
),
UpcomingEvent(
title: 'Nettoyage logs',
description: 'Archivage automatique',
date: '20 Jan 2025',
time: '03:00',
location: 'Système',
type: EventType.maintenance,
),
],
maxItems = 3,
showHeader = true,
showViewAll = true,
emptyMessage = null,
style = EventsSectionStyle.minimal;
@override
Widget build(BuildContext context) {
switch (style) {
case EventsSectionStyle.card:
return _buildCardStyle();
case EventsSectionStyle.minimal:
return _buildMinimalStyle();
case EventsSectionStyle.timeline:
return _buildTimelineStyle();
}
}
/// Style carte avec fond
Widget _buildCardStyle() {
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showHeader) _buildHeader(),
const SizedBox(height: 12),
_buildEventsList(),
],
),
);
}
/// Style minimal sans fond
Widget _buildMinimalStyle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showHeader) _buildHeader(),
const SizedBox(height: 12),
_buildEventsList(),
],
);
}
/// Style timeline avec ligne temporelle
Widget _buildTimelineStyle() {
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showHeader) _buildHeader(),
const SizedBox(height: 12),
_buildTimelineList(),
],
),
);
}
/// En-tête de la section
Widget _buildHeader() {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF6C5CE7),
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
if (showViewAll && onViewAll != null)
TextButton(
onPressed: onViewAll,
child: const Text(
'Voir tout',
style: TextStyle(
fontSize: 12,
color: Color(0xFF6C5CE7),
fontWeight: FontWeight.w600,
),
),
),
],
);
}
/// Liste des événements
Widget _buildEventsList() {
if (events.isEmpty) {
return _buildEmptyState();
}
final displayedEvents = events.take(maxItems).toList();
return Column(
children: displayedEvents.map((event) => _buildEventItem(event)).toList(),
);
}
/// Liste timeline
Widget _buildTimelineList() {
if (events.isEmpty) {
return _buildEmptyState();
}
final displayedEvents = events.take(maxItems).toList();
return Column(
children: displayedEvents.asMap().entries.map((entry) {
final index = entry.key;
final event = entry.value;
final isLast = index == displayedEvents.length - 1;
return _buildTimelineItem(event, isLast);
}).toList(),
);
}
/// Élément d'événement
Widget _buildEventItem(UpcomingEvent event) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: event.type.color.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: event.type.color.withOpacity(0.2),
width: 1,
),
),
child: InkWell(
onTap: onEventTap != null ? () => onEventTap!(event) : null,
borderRadius: BorderRadius.circular(8),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: event.type.color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
event.type.icon,
color: event.type.color,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
),
if (event.description != null) ...[
const SizedBox(height: 2),
Text(
event.description!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.access_time, size: 12, color: Colors.grey[500]),
const SizedBox(width: 4),
Text(
'${event.date} à ${event.time}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.w500,
),
),
if (event.location != null) ...[
const SizedBox(width: 8),
Icon(Icons.location_on, size: 12, color: Colors.grey[500]),
const SizedBox(width: 4),
Text(
event.location!,
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
),
),
],
],
),
],
),
),
],
),
),
);
}
/// Élément timeline
Widget _buildTimelineItem(UpcomingEvent event, bool isLast) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: event.type.color,
shape: BoxShape.circle,
),
),
if (!isLast)
Container(
width: 2,
height: 40,
color: Colors.grey[300],
),
],
),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 16),
child: _buildEventItem(event),
),
),
],
);
}
/// État vide
Widget _buildEmptyState() {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Icon(
Icons.event_available,
size: 48,
color: Colors.grey[400],
),
const SizedBox(height: 12),
Text(
emptyMessage ?? 'Aucun événement à venir',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
);
}
}
/// Modèle de données pour un événement à venir
class UpcomingEvent {
final String title;
final String? description;
final String date;
final String time;
final String? location;
final EventType type;
final Map<String, dynamic>? metadata;
const UpcomingEvent({
required this.title,
this.description,
required this.date,
required this.time,
this.location,
required this.type,
this.metadata,
});
}
/// Types d'événement
enum EventType {
meeting(Icons.meeting_room, Color(0xFF6C5CE7)),
training(Icons.school, Color(0xFF00B894)),
assembly(Icons.groups, Color(0xFF0984E3)),
maintenance(Icons.build, Color(0xFFE17055)),
deadline(Icons.schedule, Colors.orange),
celebration(Icons.celebration, Color(0xFFE84393));
const EventType(this.icon, this.color);
final IconData icon;
final Color color;
}
/// Styles de section d'événements
enum EventsSectionStyle {
card,
minimal,
timeline,
}

View File

@@ -1,17 +1,28 @@
/// Fichier d'index pour tous les widgets du dashboard
/// Facilite les imports et maintient une API propre
library dashboard_widgets;
// Export des widgets dashboard connectés
export 'connected/connected_stats_card.dart';
export 'connected/connected_recent_activities.dart';
export 'connected/connected_upcoming_events.dart';
// === WIDGETS DE SECTION ===
export 'dashboard_welcome_section.dart';
export 'dashboard_stats_grid.dart';
export 'dashboard_quick_actions_grid.dart';
export 'dashboard_recent_activity_section.dart';
export 'dashboard_insights_section.dart';
export 'dashboard_drawer.dart';
// Export des widgets charts
export 'charts/dashboard_chart_widget.dart';
// === WIDGETS ATOMIQUES ===
export 'dashboard_stats_card.dart';
export 'dashboard_quick_action_button.dart';
export 'dashboard_activity_tile.dart';
export 'dashboard_metric_row.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';