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:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -2,7 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/dashboard_entity.dart';
import '../../bloc/dashboard_bloc.dart';
import '../../../../../shared/design_system/dashboard_theme.dart';
import '../../../../../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 {
@@ -15,8 +19,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: DashboardTheme.cardDecoration,
return CoreCard(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -29,7 +33,7 @@ class DashboardNotificationsWidget extends StatelessWidget {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
return _buildNotifications(data);
return _buildNotifications(context, data);
} else if (state is DashboardError) {
return _buildErrorNotifications();
}
@@ -43,35 +47,36 @@ class DashboardNotificationsWidget extends StatelessWidget {
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue.withOpacity(0.1),
color: AppColors.primaryGreen.withOpacity(0.05),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(DashboardTheme.borderRadius),
topRight: Radius.circular(DashboardTheme.borderRadius),
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: DashboardTheme.royalBlue,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: AppColors.primaryGreen,
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.notifications,
color: DashboardTheme.white,
size: 20,
Icons.notifications_outlined,
color: Colors.white,
size: 16,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 10),
Expanded(
child: Text(
'Notifications',
style: DashboardTheme.titleMedium.copyWith(
color: DashboardTheme.royalBlue,
'NOTIFICATIONS',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
),
@@ -81,23 +86,24 @@ class DashboardNotificationsWidget extends StatelessWidget {
final data = state is DashboardLoaded
? state.dashboardData
: (state as DashboardRefreshing).dashboardData;
final urgentCount = _getUrgentNotificationsCount(data);
final urgentCount = _getUrgentNotificationsCount(context, data);
if (urgentCount > 0) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing8,
vertical: DashboardTheme.spacing4,
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: AppColors.error,
borderRadius: BorderRadius.circular(4),
),
child: Text(
urgentCount.toString(),
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 8,
),
),
);
@@ -111,8 +117,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
);
}
Widget _buildNotifications(DashboardEntity data) {
final notifications = _generateNotifications(data);
Widget _buildNotifications(BuildContext context, DashboardEntity data) {
final notifications = _generateNotifications(context, data);
if (notifications.isEmpty) {
return _buildEmptyNotifications();
@@ -127,11 +133,11 @@ class DashboardNotificationsWidget extends StatelessWidget {
Widget _buildNotificationItem(DashboardNotification notification) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: const BoxDecoration(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
color: AppColors.lightBorder,
width: 1,
),
),
@@ -140,18 +146,18 @@ class DashboardNotificationsWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(DashboardTheme.spacing8),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: notification.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
notification.icon,
color: notification.color,
size: 20,
size: 16,
),
),
const SizedBox(width: DashboardTheme.spacing12),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -161,7 +167,8 @@ class DashboardNotificationsWidget extends StatelessWidget {
Expanded(
child: Text(
notification.title,
style: DashboardTheme.bodyMedium.copyWith(
style: AppTypography.actionText.copyWith(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
@@ -169,40 +176,41 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (notification.isUrgent) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: DashboardTheme.spacing6,
vertical: DashboardTheme.spacing2,
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: DashboardTheme.error,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
color: AppColors.error,
borderRadius: BorderRadius.circular(2),
),
child: Text(
'URGENT',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.white,
style: AppTypography.badgeText.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
fontSize: 7,
),
),
),
],
],
),
const SizedBox(height: DashboardTheme.spacing4),
const SizedBox(height: 2),
Text(
notification.message,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey600,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 10,
),
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Row(
children: [
Text(
notification.timeAgo,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey500,
fontSize: 11,
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.textSecondaryLight,
fontSize: 9,
),
),
const Spacer(),
@@ -211,9 +219,10 @@ class DashboardNotificationsWidget extends StatelessWidget {
onTap: notification.onAction,
child: Text(
notification.actionLabel!,
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.royalBlue,
fontWeight: FontWeight.w600,
style: AppTypography.badgeText.copyWith(
color: AppColors.primaryGreen,
fontWeight: FontWeight.bold,
fontSize: 9,
),
),
),
@@ -229,76 +238,28 @@ class DashboardNotificationsWidget extends StatelessWidget {
}
Widget _buildLoadingNotifications() {
return Column(
children: List.generate(3, (index) {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing16),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: DashboardTheme.grey200,
width: 1,
),
),
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(DashboardTheme.borderRadiusSmall),
),
),
const SizedBox(width: DashboardTheme.spacing12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 16,
width: double.infinity,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: DashboardTheme.spacing8),
Container(
height: 12,
width: 200,
decoration: BoxDecoration(
color: DashboardTheme.grey200,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
);
}),
return const Padding(
padding: EdgeInsets.all(20),
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
);
}
Widget _buildErrorNotifications() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing24),
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icons.error_outline,
color: DashboardTheme.error,
size: 32,
color: AppColors.error,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Erreur de chargement',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.error,
'Erreur',
style: AppTypography.subtitleSmall.copyWith(
color: AppColors.error,
),
),
],
@@ -309,28 +270,23 @@ class DashboardNotificationsWidget extends StatelessWidget {
Widget _buildEmptyNotifications() {
return Container(
padding: const EdgeInsets.all(DashboardTheme.spacing24),
padding: const EdgeInsets.all(24),
child: Center(
child: Column(
children: [
const Icon(
Icons.notifications_none,
color: DashboardTheme.grey400,
size: 32,
Icons.notifications_none_outlined,
color: AppColors.textSecondaryLight,
size: 24,
),
const SizedBox(height: DashboardTheme.spacing8),
const SizedBox(height: 8),
Text(
'Aucune notification',
style: DashboardTheme.bodyMedium.copyWith(
color: DashboardTheme.grey500,
),
'AUCUNE NOTIFICATION',
style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: DashboardTheme.spacing4),
Text(
'Vous êtes à jour !',
style: DashboardTheme.bodySmall.copyWith(
color: DashboardTheme.grey400,
),
style: AppTypography.subtitleSmall.copyWith(fontSize: 10),
),
],
),
@@ -338,20 +294,20 @@ class DashboardNotificationsWidget extends StatelessWidget {
);
}
List<DashboardNotification> _generateNotifications(DashboardEntity data) {
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 nécessitent votre attention',
icon: Icons.pending_actions,
color: DashboardTheme.warning,
message: '${data.stats.pendingRequests} demandes à valider',
icon: Icons.pending_actions_outlined,
color: AppColors.warning,
timeAgo: '2h',
isUrgent: data.stats.pendingRequests > 20,
actionLabel: 'Voir',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const AdhesionsPageWrapper())),
));
}
@@ -359,13 +315,13 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (data.todayEventsCount > 0) {
notifications.add(DashboardNotification(
title: 'Événements aujourd\'hui',
message: '${data.todayEventsCount} événement(s) programmé(s) aujourd\'hui',
icon: Icons.event_available,
color: DashboardTheme.info,
message: '${data.todayEventsCount} événement(s) aujourd\'hui',
icon: Icons.event_available_outlined,
color: AppColors.info,
timeAgo: '30min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
));
}
@@ -373,9 +329,9 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (data.stats.hasGrowth) {
notifications.add(DashboardNotification(
title: 'Croissance positive',
message: 'Croissance de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
icon: Icons.trending_up,
color: DashboardTheme.success,
message: 'Progression de ${data.stats.monthlyGrowth.toStringAsFixed(1)}% ce mois',
icon: Icons.trending_up_outlined,
color: AppColors.success,
timeAgo: '1j',
isUrgent: false,
actionLabel: null,
@@ -386,14 +342,14 @@ class DashboardNotificationsWidget extends StatelessWidget {
// Notification pour l'engagement faible
if (!data.stats.isHighEngagement) {
notifications.add(DashboardNotification(
title: 'Engagement à améliorer',
message: 'Taux d\'engagement: ${(data.stats.engagementRate * 100).toStringAsFixed(0)}%',
icon: Icons.trending_down,
color: DashboardTheme.error,
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: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const SystemSettingsPage())),
));
}
@@ -401,21 +357,21 @@ class DashboardNotificationsWidget extends StatelessWidget {
if (data.recentActivitiesCount > 0) {
notifications.add(DashboardNotification(
title: 'Nouvelles activités',
message: '${data.recentActivitiesCount} nouvelles activités aujourd\'hui',
icon: Icons.fiber_new,
color: DashboardTheme.tealBlue,
message: '${data.recentActivitiesCount} activités récentes',
icon: Icons.fiber_new_outlined,
color: AppColors.brandGreen,
timeAgo: '15min',
isUrgent: false,
actionLabel: 'Voir',
onAction: () {},
onAction: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const EventsPageWrapper())),
));
}
return notifications;
}
int _getUrgentNotificationsCount(DashboardEntity data) {
final notifications = _generateNotifications(data);
int _getUrgentNotificationsCount(BuildContext context, DashboardEntity data) {
final notifications = _generateNotifications(context, data);
return notifications.where((n) => n.isUrgent).length;
}
}