Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,407 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.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 CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
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: [
Icon(
_getChartIcon(),
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
],
);
}
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: AppColors.success,
value: stats.activeMembers.toDouble(),
title: '${stats.activeMembers}',
radius: 50,
titleStyle: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
PieChartSectionData(
color: AppColors.lightBorder,
value: (stats.totalMembers - stats.activeMembers).toDouble(),
title: '${stats.totalMembers - stats.activeMembers}',
radius: 45,
titleStyle: AppTypography.badgeText.copyWith(
color: AppColors.textSecondaryLight,
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: AppColors.lightBorder,
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: AppTypography.subtitleSmall.copyWith(fontSize: 8),
);
}
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: AppTypography.subtitleSmall.copyWith(fontSize: 8),
);
},
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 5,
minY: 0,
maxY: stats.totalContributionAmount,
lineBarsData: [
LineChartBarData(
spots: _generateContributionSpots(stats),
isCurved: true,
gradient: const LinearGradient(
colors: [
AppColors.brandGreen,
AppColors.primaryGreen,
],
),
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
AppColors.brandGreen.withOpacity(0.3),
AppColors.primaryGreen.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: AppTypography.subtitleSmall.copyWith(fontSize: 8),
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
? AppColors.error
: event.isAlmostFull
? AppColors.warning
: AppColors.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 ? AppColors.success : AppColors.error,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: (stats.hasGrowth ? AppColors.success : AppColors.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: AppColors.lightBorder.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
),
),
);
}
Widget _buildErrorChart() {
return Container(
decoration: BoxDecoration(
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
size: 24,
),
const SizedBox(height: 8),
Text(
'ERREUR',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
);
}
Widget _buildEmptyChart() {
return Container(
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.bar_chart_outlined,
color: AppColors.textSecondaryLight,
size: 24,
),
const SizedBox(height: 8),
Text(
'AUCUNE DONNÉE',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
],
),
),
);
}
IconData _getChartIcon() {
switch (chartType) {
case DashboardChartType.memberActivity:
return Icons.pie_chart_outline;
case DashboardChartType.contributionTrend:
return Icons.trending_up_outlined;
case DashboardChartType.eventParticipation:
return Icons.bar_chart_outlined;
case DashboardChartType.monthlyGrowth:
return Icons.show_chart_outlined;
}
}
}
enum DashboardChartType {
memberActivity,
contributionTrend,
eventParticipation,
monthlyGrowth,
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,310 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../../../shared/widgets/mini_avatar.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../../../solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
/// Widget des activités récentes connecté au backend
class ConnectedRecentActivities extends StatelessWidget {
final int maxItems;
final VoidCallback? onSeeAll;
const ConnectedRecentActivities({
super.key,
this.maxItems = 5,
this.onSeeAll,
});
@override
Widget build(BuildContext context) {
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
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(context, data.recentActivities);
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
return _buildEmptyState();
},
),
],
),
);
}
Widget _buildHeader() {
return Row(
children: [
const Icon(
Icons.history,
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'ACTIVITÉS RÉCENTES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
if (onSeeAll != null)
GestureDetector(
onTap: onSeeAll,
child: Text(
'TOUT VOIR',
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
Widget _buildActivitiesList(BuildContext context, 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(context, activity),
if (!isLast) const SizedBox(height: 12),
],
);
}).toList(),
);
}
Widget _buildActivityItem(BuildContext context, RecentActivityEntity activity) {
return InkWell(
onTap: () => _navigateForActivity(context, activity),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MiniAvatar(
fallbackText: activity.userName.isNotEmpty ? activity.userName[0].toUpperCase() : '?',
imageUrl: activity.userAvatar,
size: 32,
),
const SizedBox(width: 12),
// Contenu
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activity.title,
style: AppTypography.actionText.copyWith(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
activity.description,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Row(
children: [
Text(
activity.userName,
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.w600,
color: AppColors.primaryGreen,
fontSize: 9,
),
),
Text(
'${activity.timeAgo}',
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
),
],
),
],
),
),
// Action button si disponible
if (activity.hasAction)
const Icon(
Icons.chevron_right,
size: 14,
color: AppColors.textSecondaryLight,
),
],
),
),
);
}
void _navigateForActivity(BuildContext context, RecentActivityEntity activity) {
final type = activity.type.toLowerCase();
Widget? page;
if (type.contains('event') || type.contains('evenement')) {
page = const EventsPageWrapper();
} else if (type.contains('member') || type.contains('membre')) {
page = const MembersPageWrapper();
} else if (type.contains('adhesion') || type.contains('adhésion')) {
page = const AdhesionsPageWrapper();
} else if (type.contains('demande') || type.contains('solidarite') || type.contains('aide')) {
page = const DemandesAidePageWrapper();
}
if (page != null) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => page!));
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(activity.title)),
);
}
}
Widget _buildLoadingList() {
return Column(
children: List.generate(3, (index) => Column(
children: [
_buildLoadingItem(),
if (index < 2) const SizedBox(height: 12),
],
)),
);
}
Widget _buildLoadingItem() {
return Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(20),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Container(
height: 12,
width: 200,
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: AppColors.lightBorder.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
);
}
Widget _buildErrorState(String message) {
return Center(
child: Column(
children: [
const Icon(Icons.error_outline, color: AppColors.error, size: 32),
const SizedBox(height: 8),
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
children: [
const Icon(Icons.history, color: AppColors.textSecondaryLight, size: 32),
const SizedBox(height: 8),
const Text('AUCUNE ACTIVITÉ', style: AppTypography.subtitleSmall),
Text('Les activités apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
],
),
);
}
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 AppColors.success;
case 'event':
return AppColors.info;
case 'contribution':
return AppColors.brandGreen;
case 'organization':
return AppColors.primaryGreen;
case 'system':
return AppColors.warning;
default:
return AppColors.textSecondaryLight;
}
}
}

View File

@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../domain/entities/dashboard_entity.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 ?? AppColors.primaryGreen;
return CoreCard(
onTap: onTap,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
Text(
value,
style: AppTypography.headerSmall.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
],
],
),
);
}
Widget _buildLoadingCard() {
return const CoreCard(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// On peut utiliser un Shimmer ici si disponible
CircularProgressIndicator(strokeWidth: 2),
],
),
);
}
Widget _buildErrorCard(String message) {
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.error_outline, color: AppColors.error, size: 20),
const SizedBox(height: 8),
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
],
),
);
}
}

View File

@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
/// 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 CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
BlocBuilder<DashboardBloc, DashboardState>(
builder: (ctx, state) {
if (state is DashboardLoading) {
return _buildLoadingList();
} else if (state is DashboardLoaded || state is DashboardRefreshing) {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildEventsList(context, data.upcomingEvents);
} else if (state is DashboardError) {
return _buildErrorState(state.message);
}
return _buildEmptyState();
},
),
],
),
);
}
Widget _buildHeader() {
return Row(
children: [
const Icon(
Icons.event_outlined,
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'ÉVÉNEMENTS À VENIR',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
),
if (onSeeAll != null)
GestureDetector(
onTap: onSeeAll,
child: Text(
'TOUT VOIR',
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
Widget _buildEventsList(BuildContext context, 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(context, event),
if (!isLast) const SizedBox(height: 12),
],
);
}).toList(),
);
}
Widget _buildEventCard(BuildContext context, UpcomingEventEntity event) {
final statusColor = event.isToday ? AppColors.success : (event.isTomorrow ? AppColors.warning : AppColors.primaryGreen);
return CoreCard(
backgroundColor: Theme.of(context).cardColor,
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: event.imageUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
event.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Icon(Icons.event_outlined, color: statusColor, size: 20),
),
)
: Icon(Icons.event_outlined, color: statusColor, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: AppTypography.actionText.copyWith(fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
const Icon(Icons.location_on_outlined, size: 10, color: AppColors.textSecondaryLight),
const SizedBox(width: 4),
Expanded(
child: Text(
event.location,
style: AppTypography.subtitleSmall.copyWith(fontSize: 9),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
event.daysUntilEvent.toUpperCase(),
style: AppTypography.badgeText.copyWith(color: statusColor, fontSize: 8, fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('PARTICIPANTS', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
Text(
'${event.currentParticipants}/${event.maxParticipants}',
style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: event.fillPercentage,
minHeight: 4,
backgroundColor: AppColors.lightBorder,
valueColor: AlwaysStoppedAnimation<Color>(
event.isFull ? AppColors.error : (event.isAlmostFull ? AppColors.warning : AppColors.success),
),
),
),
],
),
],
),
);
}
Widget _buildLoadingList() {
return Column(
children: List.generate(2, (index) => Column(
children: [
_buildLoadingCard(),
if (index < 1) const SizedBox(height: 12),
],
)),
);
}
Widget _buildLoadingCard() {
return const CoreCard(
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
Widget _buildErrorState(String message) {
return Center(
child: Column(
children: [
const Icon(Icons.error_outline, color: AppColors.error, size: 32),
const SizedBox(height: 8),
Text(message, style: AppTypography.subtitleSmall.copyWith(color: AppColors.error)),
],
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
children: [
const Icon(Icons.event_outlined, color: AppColors.textSecondaryLight, size: 32),
const SizedBox(height: 8),
const Text('AUCUN ÉVÉNEMENT', style: AppTypography.subtitleSmall),
Text('Les événements apparaîtront ici', style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
],
),
);
}
}

View File

@@ -0,0 +1,235 @@
/// Widget de menu latéral (drawer) du dashboard
/// Navigation principale de l'application
library dashboard_drawer;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
import '../../../../shared/widgets/mini_avatar.dart';
import '../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../../profile/presentation/pages/profile_page_wrapper.dart';
import '../../../notifications/presentation/pages/notifications_page_wrapper.dart';
import '../../../help/presentation/pages/help_support_page.dart';
import '../../../about/presentation/pages/about_page.dart';
/// Widget de menu latéral (Drawer / Hamburger)
///
/// Accessible via le bouton hamburger de l'AppBar.
/// Contient uniquement les menus « Mon Espace » :
/// - Mon Profil
/// - Notifications
/// - Aide & Support
/// - À propos
/// - Déconnexion
class DashboardDrawer extends StatelessWidget {
/// Callback pour les actions de navigation nommée (optionnel, non utilisé en interne)
final Function(String route)? onNavigate;
/// Callback pour la déconnexion
final VoidCallback? onLogout;
const DashboardDrawer({
super.key,
this.onNavigate,
this.onLogout,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
if (authState is! AuthAuthenticated) {
return const Drawer();
}
final state = authState;
return Drawer(
backgroundColor: ColorTokens.background,
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── En-tête utilisateur (même style que MorePage) ──────────────
_buildUserProfile(state),
const SizedBox(height: SpacingTokens.md),
// ── Section Mon Espace ─────────────────────────────────────────
_buildSectionTitle('Mon Espace'),
_buildOptionTile(
context: context,
icon: Icons.person,
title: 'Mon Profil',
subtitle: 'Modifier mes informations',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
),
),
_buildOptionTile(
context: context,
icon: Icons.notifications,
title: 'Notifications',
subtitle: 'Gérer les notifications',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NotificationsPageWrapper()),
),
),
_buildOptionTile(
context: context,
icon: Icons.help,
title: 'Aide & Support',
subtitle: 'Documentation et support',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const HelpSupportPage()),
),
),
_buildOptionTile(
context: context,
icon: Icons.info,
title: 'À propos',
subtitle: 'Version et informations',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const AboutPage()),
),
),
const SizedBox(height: SpacingTokens.md),
// ── Déconnexion ───────────────────────────────────────────────
_buildOptionTile(
context: context,
icon: Icons.logout,
title: 'Déconnexion',
subtitle: 'Se déconnecter de l\'application',
color: ColorTokens.error,
onTap: () {
Navigator.pop(context);
context.read<AuthBloc>().add(const AuthLogoutRequested());
},
),
],
),
),
),
);
},
);
}
// ── Profil utilisateur (idem MorePage._buildUserProfile) ──────────────────
Widget _buildUserProfile(AuthAuthenticated state) {
return CoreCard(
child: Row(
children: [
MiniAvatar(
fallbackText:
state.user.firstName.isNotEmpty ? state.user.firstName[0].toUpperCase() : 'U',
size: 40,
imageUrl: state.user.avatar,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${state.user.firstName} ${state.user.lastName}',
style: AppTypography.actionText,
),
Text(
state.effectiveRole.displayName.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
),
),
Text(
state.user.email,
style: AppTypography.subtitleSmall,
),
],
),
),
],
),
);
}
// ── Titre de section (idem MorePage._buildSectionTitle) ───────────────────
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(top: 24, bottom: 8, left: 4),
child: Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: AppColors.textSecondaryLight,
),
),
);
}
// ── Tuile d'option (idem MorePage._buildOptionTile) ───────────────────────
Widget _buildOptionTile({
required BuildContext context,
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
Color? color,
}) {
final effectiveColor = color ?? AppColors.primaryGreen;
return CoreCard(
margin: const EdgeInsets.only(bottom: 8),
onTap: onTap,
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: effectiveColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: effectiveColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.actionText.copyWith(
color: color ?? AppColors.textPrimaryLight,
),
),
Text(
subtitle,
style: AppTypography.subtitleSmall,
),
],
),
),
Icon(
Icons.chevron_right,
color: AppColors.textSecondaryLight,
size: 16,
),
],
),
);
}
}

View File

@@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import '../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../shared/widgets/core_card.dart';
/// Widget de statistique simple pour les dashboards de rôle
class DashboardStat extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color? color;
const DashboardStat({
super.key,
required this.title,
required this.value,
required this.icon,
this.color,
});
@override
Widget build(BuildContext context) {
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: color ?? AppColors.primaryGreen,
size: 20,
),
const Spacer(),
Text(
value,
style: AppTypography.headerSmall.copyWith(
color: color ?? AppColors.primaryGreen,
fontSize: 18,
),
),
],
),
const SizedBox(height: 8),
Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
fontSize: 10,
letterSpacing: 1.1,
),
),
],
),
);
}
}
/// Widget de grille de statistiques
class DashboardStatsGrid extends StatelessWidget {
final List<DashboardStat> stats;
final Function(String)? onStatTap;
const DashboardStatsGrid({
super.key,
required this.stats,
this.onStatTap,
});
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.3,
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: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.4,
children: children,
);
}
}
/// Widget d'action rapide
class DashboardQuickAction extends StatelessWidget {
final String title;
final IconData icon;
final VoidCallback onTap;
final Color? color;
const DashboardQuickAction({
super.key,
required this.title,
required this.icon,
required this.onTap,
this.color,
});
@override
Widget build(BuildContext context) {
return CoreCard(
onTap: onTap,
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: (color ?? AppColors.primaryGreen).withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: color ?? AppColors.primaryGreen,
size: 24,
),
),
const SizedBox(height: 12),
Text(
title,
style: AppTypography.actionText.copyWith(
fontSize: 12,
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 CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ACTIVITÉS RÉCENTES',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
const SizedBox(height: 16),
...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: 12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: (color ?? AppColors.primaryGreen).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
icon,
color: color ?? AppColors.primaryGreen,
size: 14,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.actionText.copyWith(
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
Text(
subtitle,
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
],
),
),
Text(
time,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 9,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,458 @@
import 'dart:async';
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/unionflow_design_system.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: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.brandGreen, AppColors.primaryGreen],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColors.primaryGreen.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 20),
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(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.speed_outlined,
color: Colors.white,
size: 20,
),
),
);
},
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'MÉTRIQUES TEMPS RÉEL',
style: AppTypography.actionText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 2),
Text(
'Mise à jour automatique (5 min)',
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: 10,
),
),
],
),
),
_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>(Colors.white),
),
);
}
return GestureDetector(
onTap: () {
context.read<DashboardBloc>().add(RefreshDashboardData(
organizationId: widget.organizationId,
userId: widget.userId,
));
},
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.refresh_outlined,
color: Colors.white,
size: 14,
),
),
);
},
);
}
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_outline,
AppColors.success,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildMetricItem(
'Engagement',
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
100,
Icons.favorite,
AppColors.warning,
suffix: '%',
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildMetricItem(
'Événements',
(data.stats.upcomingEvents * _countAnimation.value).round(),
data.stats.totalEvents,
Icons.event,
AppColors.info,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildMetricItem(
'Croissance',
(data.stats.monthlyGrowth * _countAnimation.value),
null,
Icons.trending_up,
data.stats.hasGrowth ? AppColors.success : AppColors.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(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.white.withOpacity(0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
icon,
color: color,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
label.toUpperCase(),
style: AppTypography.badgeText.copyWith(
color: Colors.white.withOpacity(0.8),
fontSize: 8,
),
),
),
],
),
const SizedBox(height: 8),
Text(
displayValue,
style: AppTypography.headerSmall.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
if (maxValue != null) ...[
const SizedBox(height: 2),
Text(
'sur $maxValue',
style: AppTypography.subtitleSmall.copyWith(
color: Colors.white.withOpacity(0.6),
fontSize: 8,
),
),
],
],
),
);
}
Widget _buildLoadingMetrics() {
return Column(
children: [
Row(
children: [
Expanded(child: _buildLoadingMetricItem()),
const SizedBox(width: 16),
Expanded(child: _buildLoadingMetricItem()),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildLoadingMetricItem()),
const SizedBox(width: 16),
Expanded(child: _buildLoadingMetricItem()),
],
),
],
);
}
Widget _buildLoadingMetricItem() {
return Container(
height: 100,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
);
}
Widget _buildErrorMetrics() {
return Container(
height: 200,
decoration: BoxDecoration(
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
size: 32,
),
const SizedBox(height: 8),
Text(
'Erreur de chargement',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.error,
),
),
],
),
),
);
}
Widget _buildEmptyMetrics() {
return Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.speed,
color: Colors.white.withOpacity(0.5),
size: 32,
),
const SizedBox(height: 8),
Text(
'Aucune donnée',
style: AppTypography.bodyTextSmall.copyWith(
color: Colors.white.withOpacity(0.7),
),
),
],
),
),
);
}
@override
void dispose() {
_refreshTimer?.cancel();
_pulseController.dispose();
_countController.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,511 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../data/services/dashboard_performance_monitor.dart';
/// Widget de monitoring des performances en temps réel
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 CoreCard(
margin: const EdgeInsets.all(8),
padding: EdgeInsets.zero,
child: Column(
children: [
_buildHeader(),
if (_isExpanded || widget.showDetails) ...[
const Divider(height: 1),
_buildDetailedMetrics(),
if (_recentAlerts.isNotEmpty) ...[
const Divider(height: 1),
_buildAlertsSection(),
],
],
],
),
);
}
Widget _buildLoadingWidget() {
return CoreCard(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(16),
child: Row(
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryGreen),
),
),
const SizedBox(width: 12),
Text(
'Initialisation du monitoring...',
style: AppTypography.bodyTextSmall,
),
],
),
);
}
Widget _buildHeader() {
return InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Padding(
padding: const EdgeInsets.all(16),
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: 12),
const Expanded(
child: Text(
'PERFORMANCES SYSTÈME',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: AppColors.textPrimaryLight,
),
),
),
_buildQuickMetrics(),
const SizedBox(width: 8),
Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
color: AppColors.textSecondaryLight,
size: 20,
),
],
),
),
);
}
Widget _buildQuickMetrics() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildQuickMetric(
'MEM',
'${_currentMetrics!.memoryUsage.toStringAsFixed(0)}MB',
_getMetricColor(_currentMetrics!.memoryUsage, 400, 600),
),
const SizedBox(width: 8),
_buildQuickMetric(
'CPU',
'${_currentMetrics!.cpuUsage.toStringAsFixed(0)}%',
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
),
const SizedBox(width: 8),
_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: 9,
color: AppColors.textSecondaryLight,
fontWeight: FontWeight.w500,
),
),
Text(
value,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildDetailedMetrics() {
return Padding(
padding: const EdgeInsets.all(16),
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: 12),
_buildMetricRow(
'Processeur',
'${_currentMetrics!.cpuUsage.toStringAsFixed(1)}%',
_currentMetrics!.cpuUsage / 100,
_getMetricColor(_currentMetrics!.cpuUsage, 50, 80),
Icons.speed_outlined,
),
const SizedBox(height: 12),
_buildMetricRow(
'Réseau',
'${_currentMetrics!.networkLatency} ms',
(_currentMetrics!.networkLatency / 2000).clamp(0.0, 1.0),
_getMetricColor(_currentMetrics!.networkLatency.toDouble(), 200, 1000),
Icons.wifi_outlined,
),
const SizedBox(height: 12),
_buildMetricRow(
'Images/sec',
'${_currentMetrics!.frameRate.toStringAsFixed(1)} fps',
_currentMetrics!.frameRate / 60,
_getMetricColor(60 - _currentMetrics!.frameRate, 10, 30), // Inversé car plus c'est haut, mieux c'est
Icons.videocam_outlined,
),
const SizedBox(height: 12),
_buildMetricRow(
'Batterie',
'${_currentMetrics!.batteryLevel.toStringAsFixed(0)}%',
_currentMetrics!.batteryLevel / 100,
_getBatteryColor(_currentMetrics!.batteryLevel),
Icons.battery_std_outlined,
),
],
),
);
}
Widget _buildMetricRow(
String label,
String value,
double progress,
Color color,
IconData icon,
) {
return Row(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: Text(
label,
style: AppTypography.subtitleSmall.copyWith(fontSize: 11),
),
),
Expanded(
flex: 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: progress.clamp(0.0, 1.0),
backgroundColor: AppColors.lightBorder,
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 4,
),
),
),
const SizedBox(width: 8),
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(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'ALERTES RÉCENTES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, fontSize: 10),
),
const SizedBox(height: 8),
..._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: 8),
Expanded(
child: Text(
alert.message,
style: const TextStyle(
fontSize: 11,
color: AppColors.textPrimaryLight,
),
),
),
Text(
_formatTime(alert.timestamp),
style: const TextStyle(
fontSize: 10,
color: AppColors.textSecondaryLight,
),
),
],
),
);
}
Color _getOverallHealthColor() {
if (_currentMetrics == null) return AppColors.textSecondaryLight;
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 AppColors.success;
case 1:
return AppColors.warning;
default:
return AppColors.error;
}
}
Color _getMetricColor(double value, double warningThreshold, double errorThreshold) {
if (value >= errorThreshold) return AppColors.error;
if (value >= warningThreshold) return AppColors.warning;
return AppColors.success;
}
Color _getBatteryColor(double batteryLevel) {
if (batteryLevel <= 20) return AppColors.error;
if (batteryLevel <= 50) return AppColors.warning;
return AppColors.success;
}
Color _getAlertColor(AlertSeverity severity) {
switch (severity) {
case AlertSeverity.info:
return AppColors.info;
case AlertSeverity.warning:
return AppColors.warning;
case AlertSeverity.error:
return AppColors.error;
case AlertSeverity.critical:
return AppColors.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,416 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../pages/connected_dashboard_page.dart';
import '../../pages/advanced_dashboard_page.dart';
import '../../../../settings/presentation/pages/language_settings_page.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
/// Widget de navigation pour les différents types de dashboard
class DashboardNavigation extends StatefulWidget {
final String organizationId;
final String userId;
const DashboardNavigation({
super.key,
required this.organizationId,
required this.userId,
});
@override
State<DashboardNavigation> createState() => _DashboardNavigationState();
}
class _DashboardNavigationState extends State<DashboardNavigation> {
int _currentIndex = 0;
final List<DashboardTab> _tabs = [
const DashboardTab(
title: 'Accueil',
icon: Icons.home,
activeIcon: Icons.home,
type: DashboardType.home,
),
const DashboardTab(
title: 'Analytics',
icon: Icons.analytics_outlined,
activeIcon: Icons.analytics,
type: DashboardType.analytics,
),
const DashboardTab(
title: 'Rapports',
icon: Icons.assessment_outlined,
activeIcon: Icons.assessment,
type: DashboardType.reports,
),
const DashboardTab(
title: 'Paramètres',
icon: Icons.settings_outlined,
activeIcon: Icons.settings,
type: DashboardType.settings,
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildCurrentPage(),
bottomNavigationBar: _buildBottomNavigationBar(),
floatingActionButton: _buildFloatingActionButton(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
Widget _buildCurrentPage() {
switch (_tabs[_currentIndex].type) {
case DashboardType.home:
return ConnectedDashboardPage(
organizationId: widget.organizationId,
userId: widget.userId,
);
case DashboardType.analytics:
return AdvancedDashboardPage(
organizationId: widget.organizationId,
userId: widget.userId,
);
case DashboardType.reports:
return _buildReportsPage();
case DashboardType.settings:
return _buildSettingsPage();
}
}
Widget _buildBottomNavigationBar() {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomAppBar(
shape: const CircularNotchedRectangle(),
notchMargin: 8,
color: Theme.of(context).cardColor,
elevation: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isActive = index == _currentIndex;
// Skip the middle item for FAB space
if (index == 2) {
return const SizedBox(width: 40);
}
return _buildNavItem(tab, isActive, index);
}).toList(),
),
),
),
);
}
Widget _buildNavItem(DashboardTab tab, bool isActive, int index) {
return GestureDetector(
onTap: () => setState(() => _currentIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isActive ? tab.activeIcon : tab.icon,
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
size: 20,
),
const SizedBox(height: 4),
Text(
tab.title,
style: AppTypography.badgeText.copyWith(
color: isActive ? AppColors.primaryGreen : AppColors.textSecondaryLight,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
fontSize: 9,
),
),
],
),
),
);
}
Widget _buildFloatingActionButton() {
return FloatingActionButton(
onPressed: _showQuickActions,
backgroundColor: AppColors.primaryGreen,
elevation: 4,
child: const Icon(
Icons.add_outlined,
color: Colors.white,
size: 28,
),
);
}
Widget _buildReportsPage() {
return Scaffold(
appBar: AppBar(
title: Text('Rapports'.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.1)),
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.assessment_outlined,
size: 48,
color: AppColors.textSecondaryLight,
),
const SizedBox(height: 16),
Text(
'Page Rapports'.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'En cours de développement',
style: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
),
),
],
),
),
);
}
Widget _buildSettingsPage() {
return Scaffold(
appBar: AppBar(
title: Text('Paramètres'.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.1)),
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildSettingsSection(
'Apparence',
[
_buildSettingsTile(
'Thème',
'Design System UnionFlow',
Icons.palette_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Langue',
'Français',
Icons.language_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const LanguageSettingsPage())),
),
],
),
const SizedBox(height: 24),
_buildSettingsSection(
'Notifications',
[
_buildSettingsTile(
'Notifications push',
'Activées',
Icons.notifications_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Emails',
'Quotidien',
Icons.email_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
],
),
const SizedBox(height: 24),
_buildSettingsSection(
'Données',
[
_buildSettingsTile(
'Synchronisation',
'Automatique',
Icons.sync_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
_buildSettingsTile(
'Cache',
'Vider le cache',
Icons.storage_outlined,
() => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
],
),
],
),
);
}
Widget _buildSettingsSection(String title, List<Widget> children) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.toUpperCase(),
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, color: AppColors.primaryGreen, fontSize: 10),
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.lightBorder),
),
child: Column(children: children),
),
],
);
}
Widget _buildSettingsTile(
String title,
String subtitle,
IconData icon,
VoidCallback onTap,
) {
return ListTile(
leading: Icon(icon, color: AppColors.primaryGreen, size: 20),
title: Text(title, style: AppTypography.actionText.copyWith(fontSize: 13)),
subtitle: Text(subtitle, style: AppTypography.subtitleSmall.copyWith(fontSize: 10)),
trailing: const Icon(
Icons.chevron_right_outlined,
color: AppColors.textSecondaryLight,
size: 16,
),
onTap: onTap,
);
}
void _showQuickActions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.lightBorder,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
Text(
'ACTIONS RAPIDES',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
const SizedBox(height: 20),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: [
_buildQuickActionItem(context, 'Nouveau\nMembre', Icons.person_add_outlined, AppColors.success, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper()))),
_buildQuickActionItem(context, 'Créer\nÉvénement', Icons.event_available_outlined, AppColors.primaryGreen, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper()))),
_buildQuickActionItem(context, 'Ajouter\nContribution', Icons.account_balance_wallet_outlined, AppColors.brandGreen, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper()))),
_buildQuickActionItem(context, 'Générer\nRapport', Icons.assessment_outlined, AppColors.info, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper()))),
_buildQuickActionItem(context, 'Paramètres', Icons.settings_outlined, AppColors.textSecondaryLight, () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage()))),
],
),
const SizedBox(height: 20),
],
),
),
);
}
Widget _buildQuickActionItem(BuildContext context, String title, IconData icon, Color color, VoidCallback onNavigate) {
return GestureDetector(
onTap: () {
Navigator.pop(context);
onNavigate();
},
child: Container(
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
title,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textPrimaryLight,
fontSize: 9,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}
class DashboardTab {
final String title;
final IconData icon;
final IconData activeIcon;
final DashboardType type;
const DashboardTab({
required this.title,
required this.icon,
required this.activeIcon,
required this.type,
});
}
enum DashboardType {
home,
analytics,
reports,
settings,
}

View File

@@ -0,0 +1,399 @@
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/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../../adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
/// Widget de notifications pour le dashboard
class DashboardNotificationsWidget extends StatelessWidget {
final int maxNotifications;
const DashboardNotificationsWidget({
super.key,
this.maxNotifications = 5,
});
@override
Widget build(BuildContext context) {
return CoreCard(
padding: EdgeInsets.zero,
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(context, data);
} else if (state is DashboardError) {
return _buildErrorNotifications();
}
return _buildEmptyNotifications();
},
),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withOpacity(0.05),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColors.primaryGreen,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.notifications_outlined,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
'NOTIFICATIONS',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
),
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(context, data);
if (urgentCount > 0) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.error,
borderRadius: BorderRadius.circular(4),
),
child: Text(
urgentCount.toString(),
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 8,
),
),
);
}
}
return const SizedBox.shrink();
},
),
],
),
);
}
Widget _buildNotifications(BuildContext context, DashboardEntity data) {
final notifications = _generateNotifications(context, 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(12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppColors.lightBorder,
width: 1,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: notification.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
notification.icon,
color: notification.color,
size: 16,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
notification.title,
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
if (notification.isUrgent) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: AppColors.error,
borderRadius: BorderRadius.circular(2),
),
child: Text(
'URGENT',
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 7,
),
),
),
],
],
),
const SizedBox(height: 2),
Text(
notification.message,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 10,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
notification.timeAgo,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 9,
),
),
const Spacer(),
if (notification.actionLabel != null) ...[
GestureDetector(
onTap: notification.onAction,
child: Text(
notification.actionLabel!,
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
fontSize: 9,
),
),
),
],
],
),
],
),
),
],
),
);
}
Widget _buildLoadingNotifications() {
return const Padding(
padding: EdgeInsets.all(20),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
Widget _buildErrorNotifications() {
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
size: 24,
),
const SizedBox(height: 8),
Text(
'Erreur',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.error,
),
),
],
),
),
);
}
Widget _buildEmptyNotifications() {
return Container(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icons.notifications_none_outlined,
color: AppColors.textSecondaryLight,
size: 24,
),
const SizedBox(height: 8),
Text(
'AUCUNE NOTIFICATION',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
),
Text(
'Vous êtes à jour !',
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
],
),
),
);
}
List<DashboardNotification> _generateNotifications(BuildContext context, DashboardEntity data) {
List<DashboardNotification> notifications = [];
// Notification pour les demandes en attente
if (data.stats.pendingRequests > 0) {
notifications.add(DashboardNotification(
title: 'Demandes en attente',
message: '${data.stats.pendingRequests} demandes à valider',
icon: Icons.pending_actions_outlined,
color: AppColors.warning,
timeAgo: '2h',
isUrgent: data.stats.pendingRequests > 20,
actionLabel: 'Voir',
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
));
}
// 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) aujourd\'hui',
icon: Icons.event_available_outlined,
color: AppColors.info,
timeAgo: '30min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
));
}
// Notification pour la croissance
if (data.stats.hasGrowth) {
notifications.add(DashboardNotification(
title: 'Croissance positive',
message: 'Progression de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
icon: Icons.trending_up_outlined,
color: AppColors.success,
timeAgo: '1j',
isUrgent: false,
actionLabel: null,
onAction: null,
));
}
// Notification pour l'engagement faible
if (!data.stats.isHighEngagement) {
notifications.add(DashboardNotification(
title: 'Engagement à surveiller',
message: 'Taux: ${(data.stats.engagementRate * 100).toStringAsFixed(0)}%',
icon: Icons.trending_down_outlined,
color: AppColors.error,
timeAgo: '3h',
isUrgent: data.stats.engagementRate < 0.5,
actionLabel: 'Améliorer',
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
));
}
// Notification pour les nouveaux membres
if (data.recentActivitiesCount > 0) {
notifications.add(DashboardNotification(
title: 'Nouvelles activités',
message: '${data.recentActivitiesCount} activités récentes',
icon: Icons.fiber_new_outlined,
color: AppColors.brandGreen,
timeAgo: '15min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
));
}
return notifications;
}
int _getUrgentNotificationsCount(BuildContext context, DashboardEntity data) {
final notifications = _generateNotifications(context, data);
return notifications.where((n) => n.isUrgent).length;
}
}
class DashboardNotification {
final String title;
final String message;
final IconData icon;
final Color color;
final String timeAgo;
final bool isUrgent;
final String? actionLabel;
final VoidCallback? onAction;
const DashboardNotification({
required this.title,
required this.message,
required this.icon,
required this.color,
required this.timeAgo,
required this.isUrgent,
this.actionLabel,
this.onAction,
});
}

View File

@@ -0,0 +1,348 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
/// Widget de recherche rapide pour le dashboard
class DashboardSearchWidget extends StatefulWidget {
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 = [];
List<SearchSuggestion>? _defaultSuggestions;
@override
void initState() {
super.initState();
_setupAnimations();
_setupListeners();
_filteredSuggestions = widget.suggestions ?? [];
}
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 ?? _defaultSuggestions ?? [];
});
return;
}
final defaultList = widget.suggestions ?? _defaultSuggestions ?? [];
final filtered = defaultList
.where((suggestion) =>
suggestion.title.toLowerCase().contains(query.toLowerCase()) ||
suggestion.subtitle.toLowerCase().contains(query.toLowerCase()))
.toList();
setState(() {
_filteredSuggestions = filtered;
});
}
@override
Widget build(BuildContext context) {
if (_defaultSuggestions == null) {
_defaultSuggestions = _getDefaultSuggestions(context);
if (_filteredSuggestions.isEmpty && widget.suggestions == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() => _filteredSuggestions = _defaultSuggestions!);
});
}
}
return Column(
children: [
_buildSearchBar(),
if (_isExpanded && _filteredSuggestions.isNotEmpty) ...[
const SizedBox(height: 8),
_buildSuggestions(),
],
],
);
}
Widget _buildSearchBar() {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: _isExpanded
? [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 4))]
: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2))],
),
child: TextField(
controller: _searchController,
focusNode: _focusNode,
onSubmitted: (value) {
if (value.isNotEmpty) {
widget.onSearch?.call(value);
_focusNode.unfocus();
}
},
decoration: InputDecoration(
hintText: widget.hintText ?? 'Rechercher...',
hintStyle: AppTypography.bodyTextSmall.copyWith(
color: AppColors.textSecondaryLight,
),
prefixIcon: Icon(
Icons.search_outlined,
color: _isExpanded ? AppColors.primaryGreen : AppColors.textSecondaryLight,
size: 20,
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
onPressed: () {
_searchController.clear();
_focusNode.unfocus();
},
icon: const Icon(
Icons.close_outlined,
color: AppColors.textSecondaryLight,
size: 18,
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppColors.primaryGreen,
width: 1.5,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
filled: true,
fillColor: Theme.of(context).cardColor,
),
style: AppTypography.bodyTextSmall,
),
),
);
},
);
}
Widget _buildSuggestions() {
return Container(
constraints: const BoxConstraints(maxHeight: 300),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ListView.builder(
shrinkWrap: true,
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(12),
decoration: BoxDecoration(
border: isLast
? null
: const Border(
bottom: BorderSide(
color: AppColors.lightBorder,
width: 1,
),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: suggestion.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
suggestion.icon,
color: suggestion.color,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
suggestion.title,
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
if (suggestion.subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
suggestion.subtitle,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 10,
),
),
],
],
),
),
const Icon(
Icons.chevron_right_outlined,
color: AppColors.textSecondaryLight,
size: 16,
),
],
),
),
);
}
List<SearchSuggestion> _getDefaultSuggestions(BuildContext context) {
return [
SearchSuggestion(
title: 'Membres',
subtitle: 'Rechercher des membres',
icon: Icons.people_outline,
color: AppColors.primaryGreen,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const MembersPageWrapper())),
),
SearchSuggestion(
title: 'Événements',
subtitle: 'Trouver des événements',
icon: Icons.event_outlined,
color: AppColors.brandGreen,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
),
SearchSuggestion(
title: 'Contributions',
subtitle: 'Historique des paiements',
icon: Icons.account_balance_wallet_outlined,
color: AppColors.success,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ContributionsPageWrapper())),
),
SearchSuggestion(
title: 'Rapports',
subtitle: 'Consulter les rapports',
icon: Icons.assessment_outlined,
color: AppColors.warning,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const ReportsPageWrapper())),
),
SearchSuggestion(
title: 'Paramètres',
subtitle: 'Configuration système',
icon: Icons.settings_outlined,
color: AppColors.textSecondaryLight,
onTap: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
),
];
}
@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,336 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/dashboard_theme_manager.dart';
import '../../../../../shared/design_system/tokens/app_colors.dart';
import '../../../../../shared/design_system/tokens/app_typography.dart';
import '../../../../../shared/design_system/tokens/spacing_tokens.dart';
import '../../../../../shared/design_system/tokens/radius_tokens.dart';
import '../../../../../shared/widgets/core_card.dart';
/// Widget de sélection de thème pour le Dashboard
class ThemeSelectorWidget extends StatefulWidget {
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 CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.palette,
color: AppColors.primaryGreen,
size: 24,
),
SizedBox(width: 8),
Text(
'Thème de l\'interface',
style: AppTypography.headerSmall,
),
],
),
const SizedBox(height: SpacingTokens.xl),
// Grille des thèmes
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
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: SpacingTokens.xl),
// 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(8),
border: Border.all(
color: isSelected
? themeOption.theme.primaryColor
: const Color(0xFFD1D5DB),
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
? [
BoxShadow(
color: themeOption.theme.primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
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(RadiusTokens.lg - 1),
topRight: Radius.circular(RadiusTokens.lg - 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(8),
decoration: BoxDecoration(
color: themeOption.theme.cardColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(7),
bottomRight: Radius.circular(7),
),
),
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(16),
decoration: BoxDecoration(
color: currentTheme.theme.backgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFD1D5DB)),
),
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: SpacingTokens.lg),
// Aperçu de carte avec le thème
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: currentTheme.theme.cardColor,
borderRadius: BorderRadius.circular(4),
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: SpacingTokens.lg),
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(
'Aperçu',
style: TextStyle(
fontSize: 12,
color: currentTheme.theme.textSecondary,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: currentTheme.theme.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Actif',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: currentTheme.theme.success,
),
),
),
],
),
),
const SizedBox(height: SpacingTokens.lg),
// Palette de couleurs
Row(
children: [
_buildColorSwatch('Primaire', currentTheme.theme.primaryColor),
const SizedBox(width: 8),
_buildColorSwatch('Secondaire', currentTheme.theme.secondaryColor),
const SizedBox(width: 8),
_buildColorSwatch('Succès', currentTheme.theme.success),
const SizedBox(width: 8),
_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(4),
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 10,
color: Color(0xFF4B5563),
),
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,195 @@
import 'package:flutter/material.dart';
import '../../../../../shared/design_system/unionflow_design_system.dart';
import '../../../../../shared/widgets/core_card.dart';
import '../../../../members/presentation/pages/members_page_wrapper.dart';
import '../../../../events/presentation/pages/events_page_wrapper.dart';
import '../../../../contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../../../reports/presentation/pages/reports_page_wrapper.dart';
import '../../../../settings/presentation/pages/system_settings_page.dart';
/// Widget de raccourcis rapides pour le dashboard
class DashboardShortcutsWidget extends StatelessWidget {
final List<DashboardShortcut>? customShortcuts;
final int maxShortcuts;
const DashboardShortcutsWidget({
super.key,
this.customShortcuts,
this.maxShortcuts = 6,
});
@override
Widget build(BuildContext context) {
final shortcuts = customShortcuts ?? _getDefaultShortcuts(context);
final displayShortcuts = shortcuts.take(maxShortcuts).toList();
return CoreCard(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
_buildShortcutsGrid(displayShortcuts),
],
),
);
}
Widget _buildHeader() {
return Row(
children: [
const Icon(
Icons.flash_on_outlined,
color: AppColors.primaryGreen,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'ACTIONS RAPIDES',
style: AppTypography.subtitleSmall.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
),
],
);
}
Widget _buildShortcutsGrid(List<DashboardShortcut> shortcuts) {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.9,
),
itemCount: shortcuts.length,
itemBuilder: (context, index) {
return _buildShortcutItem(shortcuts[index]);
},
);
}
Widget _buildShortcutItem(DashboardShortcut shortcut) {
return GestureDetector(
onTap: shortcut.onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: shortcut.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
shortcut.icon,
color: shortcut.color,
size: 20,
),
),
const SizedBox(height: 8),
Text(
shortcut.title,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textPrimaryLight,
fontSize: 9,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
List<DashboardShortcut> _getDefaultShortcuts(BuildContext context) {
return [
DashboardShortcut(
title: 'Nouveau\nMembre',
icon: Icons.person_add_outlined,
color: AppColors.success,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const MembersPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Créer\nÉvénement',
icon: Icons.event_available_outlined,
color: AppColors.primaryGreen,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const EventsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Ajouter\nContribution',
icon: Icons.account_balance_wallet_outlined,
color: AppColors.brandGreen,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const CotisationsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Générer\nRapport',
icon: Icons.assessment_outlined,
color: AppColors.info,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ReportsPageWrapper(),
),
);
},
),
DashboardShortcut(
title: 'Paramètres',
icon: Icons.settings_outlined,
color: AppColors.textSecondaryLight,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SystemSettingsPage(),
),
);
},
),
];
}
}
class DashboardShortcut {
final String title;
final IconData icon;
final Color color;
final VoidCallback onTap;
final String? badge;
final Color? badgeColor;
const DashboardShortcut({
required this.title,
required this.icon,
required this.color,
required this.onTap,
this.badge,
this.badgeColor,
});
}

View File

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