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

@@ -1,32 +1,28 @@
library reports_repository;
/// Repository pour la gestion des rapports et analytics
library reports_repository_impl;
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
import 'package:unionflow_mobile_apps/core/utils/logger.dart';
import '../../domain/repositories/reports_repository.dart';
import '../models/analytics_model.dart';
/// Interface du repository des rapports
abstract class ReportsRepository {
Future<List<AnalyticsModel>> getMetriques(String typeMetrique, String periode);
Future<Map<String, dynamic>> getPerformanceGlobale();
Future<List<AnalyticsModel>> getEvolutions(String typeMetrique);
Future<Map<String, dynamic>> getStatistiquesMembres();
Future<Map<String, dynamic>> getStatistiquesCotisations(int annee);
Future<Map<String, dynamic>> getStatistiquesEvenements();
}
/// Implémentation via /api/v1/analytics
class ReportsRepositoryImpl implements ReportsRepository {
final Dio _dio;
/// Implémentation du repository des rapports
@LazySingleton(as: IReportsRepository)
class ReportsRepositoryImpl implements IReportsRepository {
final ApiClient _apiClient;
static const String _analyticsBase = '/api/v1/analytics';
static const String _membresBase = '/api/membres';
static const String _cotisationsBase = '/api/cotisations';
static const String _evenementsBase = '/api/evenements';
ReportsRepositoryImpl(this._dio);
ReportsRepositoryImpl(this._apiClient);
@override
Future<List<AnalyticsModel>> getMetriques(String typeMetrique, String periode) async {
try {
final response = await _dio.get(
final response = await _apiClient.get(
'$_analyticsBase/metriques/$typeMetrique',
queryParameters: {'periodeAnalyse': periode},
);
@@ -40,7 +36,8 @@ class ReportsRepositoryImpl implements ReportsRepository {
}
}
return [];
} on DioException catch (e) {
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getMetriques échoué', error: e, stackTrace: st);
if (e.response?.statusCode == 404 || e.response?.statusCode == 400) return [];
rethrow;
}
@@ -49,12 +46,13 @@ class ReportsRepositoryImpl implements ReportsRepository {
@override
Future<Map<String, dynamic>> getPerformanceGlobale() async {
try {
final response = await _dio.get('$_analyticsBase/performance-globale');
final response = await _apiClient.get('$_analyticsBase/performance-globale');
if (response.statusCode == 200 && response.data is Map) {
return response.data as Map<String, dynamic>;
}
return {};
} on DioException {
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getPerformanceGlobale échoué', error: e, stackTrace: st);
return {};
}
}
@@ -62,7 +60,7 @@ class ReportsRepositoryImpl implements ReportsRepository {
@override
Future<List<AnalyticsModel>> getEvolutions(String typeMetrique) async {
try {
final response = await _dio.get(
final response = await _apiClient.get(
'$_analyticsBase/evolutions',
queryParameters: {'typeMetrique': typeMetrique},
);
@@ -73,7 +71,8 @@ class ReportsRepositoryImpl implements ReportsRepository {
}
}
return [];
} on DioException {
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getEvolutions échoué', error: e, stackTrace: st);
return [];
}
}
@@ -81,12 +80,13 @@ class ReportsRepositoryImpl implements ReportsRepository {
@override
Future<Map<String, dynamic>> getStatistiquesMembres() async {
try {
final response = await _dio.get('$_membresBase/statistiques');
final response = await _apiClient.get('$_membresBase/statistiques');
if (response.statusCode == 200 && response.data is Map) {
return response.data as Map<String, dynamic>;
}
return {};
} on DioException {
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getStatistiquesMembres échoué', error: e, stackTrace: st);
return {};
}
}
@@ -94,7 +94,7 @@ class ReportsRepositoryImpl implements ReportsRepository {
@override
Future<Map<String, dynamic>> getStatistiquesCotisations(int annee) async {
try {
final response = await _dio.get(
final response = await _apiClient.get(
'$_cotisationsBase/statistiques',
queryParameters: {'annee': annee},
);
@@ -102,7 +102,8 @@ class ReportsRepositoryImpl implements ReportsRepository {
return response.data as Map<String, dynamic>;
}
return {};
} on DioException {
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getStatistiquesCotisations échoué', error: e, stackTrace: st);
return {};
}
}
@@ -110,13 +111,120 @@ class ReportsRepositoryImpl implements ReportsRepository {
@override
Future<Map<String, dynamic>> getStatistiquesEvenements() async {
try {
final response = await _dio.get('$_evenementsBase/statistiques');
final response = await _apiClient.get('$_evenementsBase/statistiques');
if (response.statusCode == 200 && response.data is Map) {
return response.data as Map<String, dynamic>;
}
return {};
} on DioException {
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getStatistiquesEvenements échoué', error: e, stackTrace: st);
return {};
}
}
@override
Future<List<Map<String, dynamic>>> getAvailableReports() async {
try {
final response = await _apiClient.get('$_analyticsBase/reports/available');
if (response.statusCode == 200) {
final data = response.data;
if (data is List) {
return data.map((e) => Map<String, dynamic>.from(e as Map)).toList();
}
}
return [];
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getAvailableReports échoué', error: e, stackTrace: st);
return [];
}
}
@override
Future<void> generateReport(String type, {String? format}) async {
try {
final queryParams = <String, dynamic>{'type': type};
if (format != null) queryParams['format'] = format;
final response = await _apiClient.post(
'$_analyticsBase/reports/generate',
queryParameters: queryParams,
);
if (response.statusCode != 200 && response.statusCode != 201 && response.statusCode != 202) {
throw Exception('Generate report failed: ${response.statusCode}');
}
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: generateReport échoué', error: e, stackTrace: st);
rethrow;
}
}
@override
Future<String> exportReportPdf(String type) async {
try {
final response = await _apiClient.post(
'$_analyticsBase/reports/export',
queryParameters: {'type': type, 'format': 'pdf'},
);
if (response.statusCode == 200 && response.data is Map) {
final data = response.data as Map<String, dynamic>;
// Le backend retourne l'URL du fichier PDF généré
return data['url'] as String? ?? data['fileUrl'] as String? ?? '';
}
throw Exception('Export PDF failed: ${response.statusCode}');
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: exportReportPdf échoué', error: e, stackTrace: st);
rethrow;
}
}
@override
Future<String> exportReportExcel(String type, {String format = 'excel'}) async {
try {
final response = await _apiClient.post(
'$_analyticsBase/reports/export',
queryParameters: {'type': type, 'format': format},
);
if (response.statusCode == 200 && response.data is Map) {
final data = response.data as Map<String, dynamic>;
// Le backend retourne l'URL du fichier Excel/CSV généré
return data['url'] as String? ?? data['fileUrl'] as String? ?? '';
}
throw Exception('Export $format failed: ${response.statusCode}');
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: exportReportExcel échoué', error: e, stackTrace: st);
rethrow;
}
}
@override
Future<void> scheduleReport({String? cronExpression}) async {
try {
final response = await _apiClient.post(
'$_analyticsBase/reports/schedule',
data: cronExpression != null ? {'cronExpression': cronExpression} : null,
);
if (response.statusCode != 200 && response.statusCode != 201 && response.statusCode != 204) {
throw Exception('Schedule report failed: ${response.statusCode}');
}
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: scheduleReport échoué', error: e, stackTrace: st);
rethrow;
}
}
@override
Future<List<Map<String, dynamic>>> getScheduledReports() async {
try {
final response = await _apiClient.get('$_analyticsBase/reports/scheduled');
if (response.statusCode == 200) {
final data = response.data;
if (data is List) {
return data.map((e) => Map<String, dynamic>.from(e as Map)).toList();
}
}
return [];
} on DioException catch (e, st) {
AppLogger.error('ReportsRepository: getScheduledReports échoué', error: e, stackTrace: st);
return [];
}
}
}