Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
28
lib/features/dashboard/presentation/widgets/widgets.dart
Normal file
28
lib/features/dashboard/presentation/widgets/widgets.dart
Normal 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';
|
||||
Reference in New Issue
Block a user