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

@@ -0,0 +1,54 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../../../../core/utils/logger.dart';
import '../../data/repositories/notification_feed_repository.dart';
import 'notification_event.dart';
import 'notification_state.dart';
@injectable
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
final NotificationFeedRepository _repository;
NotificationBloc(this._repository) : super(NotificationInitial()) {
on<LoadNotificationsRequested>(_onLoadNotificationsRequested);
on<NotificationMarkedAsRead>(_onNotificationMarkedAsRead);
}
Future<void> _onLoadNotificationsRequested(LoadNotificationsRequested event, Emitter<NotificationState> emit) async {
emit(NotificationLoading());
try {
final items = await _repository.getNotifications();
emit(NotificationLoaded(items: items));
} catch (e, st) {
AppLogger.error('NotificationBloc: chargement notifications échoué', error: e, stackTrace: st);
emit(NotificationError('Erreur de chargement: $e'));
}
}
Future<void> _onNotificationMarkedAsRead(NotificationMarkedAsRead event, Emitter<NotificationState> emit) async {
if (state is NotificationLoaded) {
final currentState = state as NotificationLoaded;
try {
await _repository.markAsRead(event.id);
final updatedItems = currentState.items.map((item) {
if (item.id == event.id) {
return NotificationItem(
id: item.id,
title: item.title,
body: item.body,
date: item.date,
isRead: true,
category: item.category,
);
}
return item;
}).toList();
emit(NotificationLoaded(items: updatedItems));
} catch (e, st) {
AppLogger.error('NotificationBloc: marquer comme lu échoué', error: e, stackTrace: st);
emit(NotificationError('Impossible de marquer comme lu'));
}
}
}
}

View File

@@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
abstract class NotificationEvent extends Equatable {
const NotificationEvent();
@override
List<Object> get props => [];
}
class LoadNotificationsRequested extends NotificationEvent {}
class NotificationMarkedAsRead extends NotificationEvent {
final String id;
const NotificationMarkedAsRead(this.id);
@override
List<Object> get props => [id];
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
class NotificationItem extends Equatable {
final String id;
final String title;
final String body;
final DateTime date;
final bool isRead;
final String category; // 'finance', 'event', 'system'
const NotificationItem({
required this.id,
required this.title,
required this.body,
required this.date,
this.isRead = false,
required this.category,
});
@override
List<Object?> get props => [id, title, body, date, isRead, category];
}
abstract class NotificationState extends Equatable {
const NotificationState();
@override
List<Object?> get props => [];
}
class NotificationInitial extends NotificationState {}
class NotificationLoading extends NotificationState {}
class NotificationLoaded extends NotificationState {
final List<NotificationItem> items;
const NotificationLoaded({required this.items});
@override
List<Object?> get props => [items];
}
class NotificationError extends NotificationState {
final String message;
const NotificationError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -1,14 +1,17 @@
library notifications_bloc;
import 'package:equatable/equatable.dart';
import 'package:injectable/injectable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dio/dio.dart';
import '../../../../core/utils/logger.dart';
import '../../data/models/notification_model.dart';
import '../../data/repositories/notification_repository.dart';
part 'notifications_event.dart';
part 'notifications_state.dart';
@injectable
class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
final NotificationRepository _repository;
@@ -24,7 +27,9 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
) async {
try {
emit(const NotificationsLoading());
final notifications = await _repository.getNotificationsByMembre(event.membreId);
final notifications = (event.membreId != null && event.membreId!.isNotEmpty)
? await _repository.getNotificationsByMembre(event.membreId!)
: await _repository.getMesNotifications();
final nonLues = notifications.where((n) => !n.estLue).length;
emit(NotificationsLoaded(notifications: notifications, nonLuesCount: nonLues));
} on DioException catch (e) {
@@ -64,8 +69,9 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
final nonLues = updated.where((n) => !n.estLue).length;
emit(NotificationMarkedAsRead(notifications: updated, nonLuesCount: nonLues));
}
} catch (e) {
// Echec silencieux : ne pas bloquer l'UI
} catch (e, st) {
AppLogger.error('NotificationsBloc: marquer comme lu échoué', error: e, stackTrace: st);
emit(NotificationsError('Impossible de marquer comme lu'));
}
}

View File

@@ -8,9 +8,10 @@ abstract class NotificationsEvent extends Equatable {
}
class LoadNotifications extends NotificationsEvent {
final String membreId;
/// Si null ou vide, utilise GET /api/notifications/me (membre connecté).
final String? membreId;
final bool onlyUnread;
const LoadNotifications({required this.membreId, this.onlyUnread = false});
const LoadNotifications({this.membreId, this.onlyUnread = false});
@override
List<Object?> get props => [membreId, onlyUnread];
@@ -25,8 +26,8 @@ class MarkNotificationAsRead extends NotificationsEvent {
}
class RefreshNotifications extends NotificationsEvent {
final String membreId;
const RefreshNotifications(this.membreId);
final String? membreId;
const RefreshNotifications([this.membreId]);
@override
List<Object?> get props => [membreId];