feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:async';
|
||||
import '../../../domain/entities/dashboard_entity.dart';
|
||||
import '../../bloc/dashboard_bloc.dart';
|
||||
import '../../../../../shared/design_system/dashboard_theme.dart';
|
||||
import '../../../../../shared/design_system/unionflow_design_system.dart';
|
||||
|
||||
/// Widget de métriques en temps réel avec animations
|
||||
class RealTimeMetricsWidget extends StatefulWidget {
|
||||
@@ -81,13 +82,27 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: DashboardTheme.gradientCardDecoration,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryGreen.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: DashboardTheme.spacing20),
|
||||
const SizedBox(height: 20),
|
||||
BlocConsumer<DashboardBloc, DashboardState>(
|
||||
listener: (context, state) {
|
||||
if (state is DashboardLoaded) {
|
||||
@@ -122,37 +137,39 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
return Transform.scale(
|
||||
scale: _pulseAnimation.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing8),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white,
|
||||
size: 24,
|
||||
Icons.speed_outlined,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing12),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Métriques Temps Réel',
|
||||
style: DashboardTheme.titleMedium.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
'MÉTRIQUES TEMPS RÉEL',
|
||||
style: AppTypography.actionText.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Mise à jour automatique',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
'Mise à jour automatique (5 min)',
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -172,7 +189,7 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -185,15 +202,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing4),
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.refresh,
|
||||
color: DashboardTheme.white,
|
||||
size: 16,
|
||||
Icons.refresh_outlined,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -211,27 +228,27 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Membres Actifs',
|
||||
'MEMBRES ACTIFS',
|
||||
(data.stats.activeMembers * _countAnimation.value).round(),
|
||||
data.stats.totalMembers,
|
||||
Icons.people,
|
||||
DashboardTheme.success,
|
||||
Icons.people_outline,
|
||||
AppColors.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Engagement',
|
||||
((data.stats.engagementRate * 100) * _countAnimation.value).round(),
|
||||
100,
|
||||
Icons.favorite,
|
||||
DashboardTheme.warning,
|
||||
AppColors.warning,
|
||||
suffix: '%',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -240,17 +257,17 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
(data.stats.upcomingEvents * _countAnimation.value).round(),
|
||||
data.stats.totalEvents,
|
||||
Icons.event,
|
||||
DashboardTheme.info,
|
||||
AppColors.info,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricItem(
|
||||
'Croissance',
|
||||
(data.stats.monthlyGrowth * _countAnimation.value),
|
||||
null,
|
||||
Icons.trending_up,
|
||||
data.stats.hasGrowth ? DashboardTheme.success : DashboardTheme.error,
|
||||
data.stats.hasGrowth ? AppColors.success : AppColors.error,
|
||||
suffix: '%',
|
||||
isDecimal: true,
|
||||
),
|
||||
@@ -280,12 +297,12 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: DashboardTheme.white.withOpacity(0.2),
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
@@ -296,34 +313,36 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: DashboardTheme.spacing8),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.8),
|
||||
label.toUpperCase(),
|
||||
style: AppTypography.badgeText.copyWith(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
displayValue,
|
||||
style: DashboardTheme.titleLarge.copyWith(
|
||||
color: DashboardTheme.white,
|
||||
style: AppTypography.headerSmall.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 24,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
if (maxValue != null) ...[
|
||||
const SizedBox(height: DashboardTheme.spacing4),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'sur $maxValue',
|
||||
style: DashboardTheme.bodySmall.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.6),
|
||||
style: AppTypography.subtitleSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -338,15 +357,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing16),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
const SizedBox(width: DashboardTheme.spacing16),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: _buildLoadingMetricItem()),
|
||||
],
|
||||
),
|
||||
@@ -357,15 +376,15 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
Widget _buildLoadingMetricItem() {
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(DashboardTheme.spacing16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(DashboardTheme.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -375,8 +394,8 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: AppColors.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
@@ -384,14 +403,14 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: DashboardTheme.error,
|
||||
color: AppColors.error,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.error,
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -404,8 +423,8 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: DashboardTheme.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DashboardTheme.borderRadius),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
@@ -413,14 +432,14 @@ class _RealTimeMetricsWidgetState extends State<RealTimeMetricsWidget>
|
||||
children: [
|
||||
Icon(
|
||||
Icons.speed,
|
||||
color: DashboardTheme.white.withOpacity(0.5),
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: DashboardTheme.spacing8),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucune donnée',
|
||||
style: DashboardTheme.bodyMedium.copyWith(
|
||||
color: DashboardTheme.white.withOpacity(0.7),
|
||||
style: AppTypography.bodyTextSmall.copyWith(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user