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,400 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../../config/dashboard_config.dart';
|
||||
|
||||
/// Gestionnaire de cache avancé pour le Dashboard
|
||||
class DashboardCacheManager {
|
||||
static const String _keyPrefix = 'dashboard_cache_';
|
||||
static const String _keyDashboardData = '${_keyPrefix}data';
|
||||
static const String _keyDashboardStats = '${_keyPrefix}stats';
|
||||
static const String _keyRecentActivities = '${_keyPrefix}activities';
|
||||
static const String _keyUpcomingEvents = '${_keyPrefix}events';
|
||||
static const String _keyLastUpdate = '${_keyPrefix}last_update';
|
||||
static const String _keyUserPreferences = '${_keyPrefix}user_prefs';
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
final Map<String, dynamic> _memoryCache = {};
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
Timer? _cleanupTimer;
|
||||
|
||||
/// Initialise le gestionnaire de cache
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_startCleanupTimer();
|
||||
await _loadMemoryCache();
|
||||
}
|
||||
|
||||
/// Démarre le timer de nettoyage automatique
|
||||
void _startCleanupTimer() {
|
||||
_cleanupTimer = Timer.periodic(
|
||||
const Duration(minutes: 30),
|
||||
(_) => _cleanupExpiredCache(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Charge le cache en mémoire au démarrage
|
||||
Future<void> _loadMemoryCache() async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final keys = _prefs!.getKeys().where((key) => key.startsWith(_keyPrefix));
|
||||
|
||||
for (final key in keys) {
|
||||
final value = _prefs!.getString(key);
|
||||
if (value != null) {
|
||||
try {
|
||||
final data = jsonDecode(value);
|
||||
_memoryCache[key] = data;
|
||||
|
||||
// Charger le timestamp si disponible
|
||||
final timestampKey = '${key}_timestamp';
|
||||
final timestamp = _prefs!.getInt(timestampKey);
|
||||
if (timestamp != null) {
|
||||
_cacheTimestamps[key] = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
}
|
||||
} catch (e) {
|
||||
// Supprimer les données corrompues
|
||||
await _prefs!.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde les données complètes du dashboard
|
||||
Future<void> cacheDashboardData(
|
||||
DashboardDataModel data,
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyDashboardData}_${organizationId}_$userId';
|
||||
await _cacheData(key, data.toJson());
|
||||
}
|
||||
|
||||
/// Récupère les données complètes du dashboard
|
||||
Future<DashboardDataModel?> getCachedDashboardData(
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyDashboardData}_${organizationId}_$userId';
|
||||
final data = await _getCachedData(key);
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
return DashboardDataModel.fromJson(data);
|
||||
} catch (e) {
|
||||
// Supprimer les données corrompues
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sauvegarde les statistiques du dashboard
|
||||
Future<void> cacheDashboardStats(
|
||||
DashboardStatsModel stats,
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyDashboardStats}_${organizationId}_$userId';
|
||||
await _cacheData(key, stats.toJson());
|
||||
}
|
||||
|
||||
/// Récupère les statistiques du dashboard
|
||||
Future<DashboardStatsModel?> getCachedDashboardStats(
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyDashboardStats}_${organizationId}_$userId';
|
||||
final data = await _getCachedData(key);
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
return DashboardStatsModel.fromJson(data);
|
||||
} catch (e) {
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sauvegarde les activités récentes
|
||||
Future<void> cacheRecentActivities(
|
||||
List<RecentActivityModel> activities,
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyRecentActivities}_${organizationId}_$userId';
|
||||
final data = activities.map((activity) => activity.toJson()).toList();
|
||||
await _cacheData(key, data);
|
||||
}
|
||||
|
||||
/// Récupère les activités récentes
|
||||
Future<List<RecentActivityModel>?> getCachedRecentActivities(
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyRecentActivities}_${organizationId}_$userId';
|
||||
final data = await _getCachedData(key);
|
||||
|
||||
if (data != null && data is List) {
|
||||
try {
|
||||
return data
|
||||
.map((item) => RecentActivityModel.fromJson(item))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sauvegarde les événements à venir
|
||||
Future<void> cacheUpcomingEvents(
|
||||
List<UpcomingEventModel> events,
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyUpcomingEvents}_${organizationId}_$userId';
|
||||
final data = events.map((event) => event.toJson()).toList();
|
||||
await _cacheData(key, data);
|
||||
}
|
||||
|
||||
/// Récupère les événements à venir
|
||||
Future<List<UpcomingEventModel>?> getCachedUpcomingEvents(
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyUpcomingEvents}_${organizationId}_$userId';
|
||||
final data = await _getCachedData(key);
|
||||
|
||||
if (data != null && data is List) {
|
||||
try {
|
||||
return data
|
||||
.map((item) => UpcomingEventModel.fromJson(item))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sauvegarde les préférences utilisateur
|
||||
Future<void> cacheUserPreferences(
|
||||
Map<String, dynamic> preferences,
|
||||
String userId,
|
||||
) async {
|
||||
final key = '${_keyUserPreferences}_$userId';
|
||||
await _cacheData(key, preferences);
|
||||
}
|
||||
|
||||
/// Récupère les préférences utilisateur
|
||||
Future<Map<String, dynamic>?> getCachedUserPreferences(String userId) async {
|
||||
final key = '${_keyUserPreferences}_$userId';
|
||||
final data = await _getCachedData(key);
|
||||
|
||||
if (data != null && data is Map<String, dynamic>) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Méthode générique pour sauvegarder des données
|
||||
Future<void> _cacheData(String key, dynamic data) async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
try {
|
||||
final jsonString = jsonEncode(data);
|
||||
await _prefs!.setString(key, jsonString);
|
||||
|
||||
// Sauvegarder le timestamp
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
await _prefs!.setInt('${key}_timestamp', timestamp);
|
||||
|
||||
// Mettre à jour le cache mémoire
|
||||
_memoryCache[key] = data;
|
||||
_cacheTimestamps[key] = DateTime.now();
|
||||
|
||||
} catch (e) {
|
||||
// Erreur de sérialisation, ignorer
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode générique pour récupérer des données
|
||||
Future<dynamic> _getCachedData(String key) async {
|
||||
// Vérifier d'abord le cache mémoire
|
||||
if (_memoryCache.containsKey(key)) {
|
||||
if (_isCacheValid(key)) {
|
||||
return _memoryCache[key];
|
||||
} else {
|
||||
// Cache expiré, le supprimer
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier le cache persistant
|
||||
if (_prefs == null) return null;
|
||||
|
||||
final jsonString = _prefs!.getString(key);
|
||||
if (jsonString != null) {
|
||||
try {
|
||||
final data = jsonDecode(jsonString);
|
||||
|
||||
// Vérifier la validité du cache
|
||||
if (_isCacheValid(key)) {
|
||||
// Charger en mémoire pour les prochains accès
|
||||
_memoryCache[key] = data;
|
||||
return data;
|
||||
} else {
|
||||
// Cache expiré, le supprimer
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
// Données corrompues, les supprimer
|
||||
await _removeCachedData(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Vérifie si le cache est encore valide
|
||||
bool _isCacheValid(String key) {
|
||||
final timestamp = _cacheTimestamps[key];
|
||||
if (timestamp == null) {
|
||||
// Essayer de récupérer le timestamp depuis SharedPreferences
|
||||
final timestampMs = _prefs?.getInt('${key}_timestamp');
|
||||
if (timestampMs != null) {
|
||||
final cacheTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
|
||||
_cacheTimestamps[key] = cacheTime;
|
||||
return DateTime.now().difference(cacheTime) < DashboardConfig.cacheExpiration;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return DateTime.now().difference(timestamp) < DashboardConfig.cacheExpiration;
|
||||
}
|
||||
|
||||
/// Supprime des données du cache
|
||||
Future<void> _removeCachedData(String key) async {
|
||||
_memoryCache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
|
||||
if (_prefs != null) {
|
||||
await _prefs!.remove(key);
|
||||
await _prefs!.remove('${key}_timestamp');
|
||||
}
|
||||
}
|
||||
|
||||
/// Nettoie le cache expiré
|
||||
Future<void> _cleanupExpiredCache() async {
|
||||
final keysToRemove = <String>[];
|
||||
|
||||
for (final key in _cacheTimestamps.keys) {
|
||||
if (!_isCacheValid(key)) {
|
||||
keysToRemove.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
await _removeCachedData(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Vide tout le cache
|
||||
Future<void> clearCache() async {
|
||||
_memoryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
|
||||
if (_prefs != null) {
|
||||
final keys = _prefs!.getKeys().where((key) => key.startsWith(_keyPrefix));
|
||||
for (final key in keys) {
|
||||
await _prefs!.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Vide le cache pour un utilisateur spécifique
|
||||
Future<void> clearUserCache(String organizationId, String userId) async {
|
||||
final userKeys = [
|
||||
'${_keyDashboardData}_${organizationId}_$userId',
|
||||
'${_keyDashboardStats}_${organizationId}_$userId',
|
||||
'${_keyRecentActivities}_${organizationId}_$userId',
|
||||
'${_keyUpcomingEvents}_${organizationId}_$userId',
|
||||
'${_keyUserPreferences}_$userId',
|
||||
];
|
||||
|
||||
for (final key in userKeys) {
|
||||
await _removeCachedData(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient les statistiques du cache
|
||||
Map<String, dynamic> getCacheStats() {
|
||||
final totalKeys = _memoryCache.length;
|
||||
final validKeys = _cacheTimestamps.keys.where(_isCacheValid).length;
|
||||
final expiredKeys = totalKeys - validKeys;
|
||||
|
||||
return {
|
||||
'totalKeys': totalKeys,
|
||||
'validKeys': validKeys,
|
||||
'expiredKeys': expiredKeys,
|
||||
'memoryUsage': _calculateMemoryUsage(),
|
||||
'oldestEntry': _getOldestEntryAge(),
|
||||
'newestEntry': _getNewestEntryAge(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Calcule l'utilisation mémoire approximative
|
||||
int _calculateMemoryUsage() {
|
||||
int totalSize = 0;
|
||||
for (final data in _memoryCache.values) {
|
||||
try {
|
||||
totalSize += jsonEncode(data).length;
|
||||
} catch (e) {
|
||||
// Ignorer les erreurs de sérialisation
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/// Obtient l'âge de l'entrée la plus ancienne
|
||||
Duration? _getOldestEntryAge() {
|
||||
if (_cacheTimestamps.isEmpty) return null;
|
||||
|
||||
final oldestTimestamp = _cacheTimestamps.values
|
||||
.reduce((a, b) => a.isBefore(b) ? a : b);
|
||||
|
||||
return DateTime.now().difference(oldestTimestamp);
|
||||
}
|
||||
|
||||
/// Obtient l'âge de l'entrée la plus récente
|
||||
Duration? _getNewestEntryAge() {
|
||||
if (_cacheTimestamps.isEmpty) return null;
|
||||
|
||||
final newestTimestamp = _cacheTimestamps.values
|
||||
.reduce((a, b) => a.isAfter(b) ? a : b);
|
||||
|
||||
return DateTime.now().difference(newestTimestamp);
|
||||
}
|
||||
|
||||
/// Libère les ressources
|
||||
void dispose() {
|
||||
_cleanupTimer?.cancel();
|
||||
_memoryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,36 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../../core/network/api_client.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../../../../core/network/dio_client.dart';
|
||||
import '../models/membre_dashboard_synthese_model.dart';
|
||||
import '../models/compte_adherent_model.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
|
||||
abstract class DashboardRemoteDataSource {
|
||||
Future<DashboardDataModel> getDashboardData(String organizationId, String userId);
|
||||
/// Dashboard personnel du membre connecté (sans organisationId). GET /api/dashboard/membre/me
|
||||
Future<MembreDashboardSyntheseModel> getMemberDashboardData();
|
||||
/// Synthèse des cotisations du membre connecté. GET /api/cotisations/mes-cotisations/synthese
|
||||
/// Utilisé en fallback quand les montants de getMemberDashboardData() sont à 0.
|
||||
Future<Map<String, dynamic>?> getMesCotisationsSynthese();
|
||||
/// Compte adhérent unifié (soldes, crédits, capacité d'emprunt). GET /api/membres/mon-compte
|
||||
Future<CompteAdherentModel> getCompteAdherent();
|
||||
Future<DashboardStatsModel> getDashboardStats(String organizationId, String userId);
|
||||
Future<List<RecentActivityModel>> getRecentActivities(String organizationId, String userId, {int limit = 10});
|
||||
Future<List<UpcomingEventModel>> getUpcomingEvents(String organizationId, String userId, {int limit = 5});
|
||||
}
|
||||
|
||||
@Injectable(as: DashboardRemoteDataSource)
|
||||
class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
final DioClient dioClient;
|
||||
final ApiClient apiClient;
|
||||
|
||||
DashboardRemoteDataSourceImpl({required this.dioClient});
|
||||
DashboardRemoteDataSourceImpl(this.apiClient);
|
||||
|
||||
@override
|
||||
Future<DashboardDataModel> getDashboardData(String organizationId, String userId) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/data',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -32,16 +44,77 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load dashboard data: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardData', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardData', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreDashboardSyntheseModel> getMemberDashboardData() async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/dashboard/membre/me');
|
||||
if (response.statusCode == 200) {
|
||||
return MembreDashboardSyntheseModel.fromJson(
|
||||
response.data is Map<String, dynamic> ? response.data as Map<String, dynamic> : Map<String, dynamic>.from(response.data as Map),
|
||||
);
|
||||
} else {
|
||||
throw ServerException('Failed to load member dashboard: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getMemberDashboardData', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getMemberDashboardData', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getMesCotisationsSynthese() async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/cotisations/mes-cotisations/synthese');
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return response.data is Map<String, dynamic>
|
||||
? response.data as Map<String, dynamic>
|
||||
: Map<String, dynamic>.from(response.data as Map);
|
||||
}
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getMesCotisationsSynthese échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
|
||||
Future<CompteAdherentModel> getCompteAdherent() async {
|
||||
try {
|
||||
final response = await apiClient.get('/api/membres/mon-compte');
|
||||
if (response.statusCode == 200) {
|
||||
return CompteAdherentModel.fromJson(
|
||||
response.data is Map<String, dynamic> ? response.data as Map<String, dynamic> : Map<String, dynamic>.from(response.data as Map),
|
||||
);
|
||||
} else {
|
||||
throw ServerException('Failed to load adherent account: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getCompteAdherent', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getCompteAdherent', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DashboardStatsModel> getDashboardStats(String organizationId, String userId) async {
|
||||
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/stats',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -55,9 +128,11 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load dashboard stats: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardStats', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getDashboardStats', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +143,7 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/activities',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -84,9 +159,11 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load recent activities: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getRecentActivities', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getRecentActivities', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +174,7 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
int limit = 5,
|
||||
}) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
final response = await apiClient.get(
|
||||
'/api/v1/dashboard/events/upcoming',
|
||||
queryParameters: {
|
||||
'organizationId': organizationId,
|
||||
@@ -113,9 +190,11 @@ class DashboardRemoteDataSourceImpl implements DashboardRemoteDataSource {
|
||||
throw ServerException('Failed to load upcoming events: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getUpcomingEvents', error: e);
|
||||
throw ServerException('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw ServerException('Unexpected error: $e');
|
||||
} catch (e, st) {
|
||||
AppLogger.error('DashboardRemoteDataSource: getUpcomingEvents', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/// Modèle pour le "compte adhérent" unifié (GET /api/membres/mon-compte).
|
||||
class CompteAdherentModel {
|
||||
final String numeroMembre;
|
||||
final String nomComplet;
|
||||
final String? organisationNom;
|
||||
final String? dateAdhesion;
|
||||
final String statutCompte;
|
||||
|
||||
final double soldeCotisations;
|
||||
final double soldeEpargne;
|
||||
final double soldeBloque;
|
||||
final double soldeTotalDisponible;
|
||||
final double encoursCreditTotal;
|
||||
final double capaciteEmprunt;
|
||||
|
||||
final int nombreCotisationsPayees;
|
||||
final int nombreCotisationsTotal;
|
||||
final int nombreCotisationsEnRetard;
|
||||
final int? tauxEngagement;
|
||||
|
||||
final int nombreComptesEpargne;
|
||||
final String dateCalcul;
|
||||
|
||||
const CompteAdherentModel({
|
||||
required this.numeroMembre,
|
||||
required this.nomComplet,
|
||||
this.organisationNom,
|
||||
this.dateAdhesion,
|
||||
this.statutCompte = 'ACTIF',
|
||||
this.soldeCotisations = 0,
|
||||
this.soldeEpargne = 0,
|
||||
this.soldeBloque = 0,
|
||||
this.soldeTotalDisponible = 0,
|
||||
this.encoursCreditTotal = 0,
|
||||
this.capaciteEmprunt = 0,
|
||||
this.nombreCotisationsPayees = 0,
|
||||
this.nombreCotisationsTotal = 0,
|
||||
this.nombreCotisationsEnRetard = 0,
|
||||
this.tauxEngagement,
|
||||
this.nombreComptesEpargne = 0,
|
||||
required this.dateCalcul,
|
||||
});
|
||||
|
||||
factory CompteAdherentModel.fromJson(Map<String, dynamic> json) {
|
||||
return CompteAdherentModel(
|
||||
numeroMembre: json['numeroMembre'] as String? ?? 'N/A',
|
||||
nomComplet: json['nomComplet'] as String? ?? '',
|
||||
organisationNom: json['organisationNom'] as String?,
|
||||
dateAdhesion: json['dateAdhesion'] as String?,
|
||||
statutCompte: json['statutCompte'] as String? ?? 'ACTIF',
|
||||
soldeCotisations: _toDouble(json['soldeCotisations']),
|
||||
soldeEpargne: _toDouble(json['soldeEpargne']),
|
||||
soldeBloque: _toDouble(json['soldeBloque']),
|
||||
soldeTotalDisponible: _toDouble(json['soldeTotalDisponible']),
|
||||
encoursCreditTotal: _toDouble(json['encoursCreditTotal']),
|
||||
capaciteEmprunt: _toDouble(json['capaciteEmprunt']),
|
||||
nombreCotisationsPayees: (json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0,
|
||||
nombreCotisationsTotal: (json['nombreCotisationsTotal'] as num?)?.toInt() ?? 0,
|
||||
nombreCotisationsEnRetard: (json['nombreCotisationsEnRetard'] as num?)?.toInt() ?? 0,
|
||||
tauxEngagement: (json['tauxEngagement'] as num?)?.toInt(),
|
||||
nombreComptesEpargne: (json['nombreComptesEpargne'] as num?)?.toInt() ?? 0,
|
||||
dateCalcul: json['dateCalcul'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ class DashboardStatsModel extends Equatable {
|
||||
final double monthlyGrowth;
|
||||
final double engagementRate;
|
||||
final DateTime lastUpdated;
|
||||
final int? totalOrganizations;
|
||||
final Map<String, int>? organizationTypeDistribution;
|
||||
|
||||
const DashboardStatsModel({
|
||||
required this.totalMembers,
|
||||
@@ -30,6 +32,8 @@ class DashboardStatsModel extends Equatable {
|
||||
required this.monthlyGrowth,
|
||||
required this.engagementRate,
|
||||
required this.lastUpdated,
|
||||
this.totalOrganizations,
|
||||
this.organizationTypeDistribution,
|
||||
});
|
||||
|
||||
factory DashboardStatsModel.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -63,6 +67,8 @@ class DashboardStatsModel extends Equatable {
|
||||
monthlyGrowth,
|
||||
engagementRate,
|
||||
lastUpdated,
|
||||
totalOrganizations,
|
||||
organizationTypeDistribution,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ DashboardStatsModel _$DashboardStatsModelFromJson(Map<String, dynamic> json) =>
|
||||
monthlyGrowth: (json['monthlyGrowth'] as num).toDouble(),
|
||||
engagementRate: (json['engagementRate'] as num).toDouble(),
|
||||
lastUpdated: DateTime.parse(json['lastUpdated'] as String),
|
||||
totalOrganizations: (json['totalOrganizations'] as num?)?.toInt(),
|
||||
organizationTypeDistribution:
|
||||
(json['organizationTypeDistribution'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DashboardStatsModelToJson(
|
||||
@@ -36,6 +41,8 @@ Map<String, dynamic> _$DashboardStatsModelToJson(
|
||||
'monthlyGrowth': instance.monthlyGrowth,
|
||||
'engagementRate': instance.engagementRate,
|
||||
'lastUpdated': instance.lastUpdated.toIso8601String(),
|
||||
'totalOrganizations': instance.totalOrganizations,
|
||||
'organizationTypeDistribution': instance.organizationTypeDistribution,
|
||||
};
|
||||
|
||||
RecentActivityModel _$RecentActivityModelFromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -11,6 +11,8 @@ class MembreDashboardSyntheseModel {
|
||||
final double totalCotisationsPayeesToutTemps;
|
||||
/// Nombre de cotisations payées (pour carte « Cotisations »).
|
||||
final int nombreCotisationsPayees;
|
||||
/// Nombre total de cotisations (toutes années, tous statuts).
|
||||
final int nombreCotisationsTotal;
|
||||
final String statutCotisations;
|
||||
final int? tauxCotisationsPerso;
|
||||
final double monSoldeEpargne;
|
||||
@@ -32,6 +34,7 @@ class MembreDashboardSyntheseModel {
|
||||
this.totalCotisationsPayeesAnnee = 0,
|
||||
this.totalCotisationsPayeesToutTemps = 0,
|
||||
this.nombreCotisationsPayees = 0,
|
||||
this.nombreCotisationsTotal = 0,
|
||||
this.statutCotisations = 'À jour',
|
||||
this.tauxCotisationsPerso,
|
||||
this.monSoldeEpargne = 0,
|
||||
@@ -55,6 +58,8 @@ class MembreDashboardSyntheseModel {
|
||||
totalCotisationsPayeesAnnee: _toDouble(json['totalCotisationsPayeesAnnee']),
|
||||
totalCotisationsPayeesToutTemps: _toDouble(json['totalCotisationsPayeesToutTemps']),
|
||||
nombreCotisationsPayees: (json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0,
|
||||
nombreCotisationsTotal: (json['nombreCotisationsTotal'] as num?)?.toInt() ??
|
||||
(json['nombreCotisationsPayees'] as num?)?.toInt() ?? 0,
|
||||
statutCotisations: json['statutCotisations'] as String? ?? 'À jour',
|
||||
tauxCotisationsPerso: (json['tauxCotisationsPerso'] as num?)?.toInt(),
|
||||
monSoldeEpargne: _toDouble(json['monSoldeEpargne']),
|
||||
@@ -70,6 +75,7 @@ class MembreDashboardSyntheseModel {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/dashboard_entity.dart';
|
||||
import '../../domain/entities/compte_adherent_entity.dart';
|
||||
import '../../domain/repositories/dashboard_repository.dart';
|
||||
import '../datasources/dashboard_remote_datasource.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../models/membre_dashboard_synthese_model.dart';
|
||||
import '../models/compte_adherent_model.dart';
|
||||
import '../../../../core/error/exceptions.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/network/network_info.dart';
|
||||
@@ -19,6 +21,21 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
required this.networkInfo,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, CompteAdherentEntity>> getCompteAdherent() async {
|
||||
if (!await networkInfo.isConnected) {
|
||||
return const Left(NetworkFailure('No internet connection'));
|
||||
}
|
||||
try {
|
||||
final model = await remoteDataSource.getCompteAdherent();
|
||||
return Right(_mapCompteToEntity(model));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} catch (e) {
|
||||
return Left(ServerFailure('Unexpected error: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, DashboardEntity>> getDashboardData(
|
||||
String organizationId,
|
||||
@@ -31,9 +48,32 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
// Membre sans contexte org : utiliser l'API dashboard membre (GET /api/dashboard/membre/me)
|
||||
final useMemberDashboard = organizationId.trim().isEmpty;
|
||||
if (useMemberDashboard) {
|
||||
final synthese = await remoteDataSource.getMemberDashboardData();
|
||||
return Right(_mapMemberSyntheseToEntity(synthese, userId));
|
||||
// Chargement parallèle de la synthèse et du compte adhérent unifié
|
||||
final results = await Future.wait([
|
||||
remoteDataSource.getMemberDashboardData(),
|
||||
remoteDataSource.getCompteAdherent(),
|
||||
]);
|
||||
|
||||
final synthese = results[0] as MembreDashboardSyntheseModel;
|
||||
final compteModel = results[1] as CompteAdherentModel;
|
||||
|
||||
// Fallback : si les montants sont à zéro mais qu'il y a des cotisations,
|
||||
// on complète avec /api/cotisations/mes-cotisations/synthese
|
||||
Map<String, dynamic>? cotSynthese;
|
||||
if (synthese.totalCotisationsPayeesToutTemps == 0 ||
|
||||
synthese.tauxCotisationsPerso == null ||
|
||||
(synthese.tauxCotisationsPerso ?? 0) == 0) {
|
||||
cotSynthese = await remoteDataSource.getMesCotisationsSynthese();
|
||||
}
|
||||
|
||||
return Right(_mapMemberSyntheseToEntity(
|
||||
synthese,
|
||||
userId,
|
||||
cotSynthese: cotSynthese,
|
||||
compteModel: compteModel,
|
||||
));
|
||||
}
|
||||
|
||||
final dashboardData = await remoteDataSource.getDashboardData(organizationId, userId);
|
||||
return Right(_mapToEntity(dashboardData));
|
||||
} on ServerException catch (e) {
|
||||
@@ -43,24 +83,65 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Construit une DashboardEntity à partir de la synthèse membre (même structure pour réutiliser l'UI).
|
||||
DashboardEntity _mapMemberSyntheseToEntity(MembreDashboardSyntheseModel s, String userId) {
|
||||
/// Construit une DashboardEntity à partir de la synthèse membre.
|
||||
/// [cotSynthese] est optionnel : utilisé en fallback quand les montants du dashboard
|
||||
/// membre sont à zéro (incohérence backend entre /api/dashboard/membre/me
|
||||
/// et /api/cotisations/mes-cotisations/synthese).
|
||||
DashboardEntity _mapMemberSyntheseToEntity(
|
||||
MembreDashboardSyntheseModel s,
|
||||
String userId, {
|
||||
Map<String, dynamic>? cotSynthese,
|
||||
CompteAdherentModel? compteModel,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
// Contribution Totale = cotisations payées tout temps ; MON SOLDE TOTAL = cotisations tout temps + épargne
|
||||
final totalCotisationsToutTemps = s.totalCotisationsPayeesToutTemps;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Montant des cotisations payées tout temps
|
||||
// ------------------------------------------------------------------
|
||||
double totalCotisationsToutTemps = s.totalCotisationsPayeesToutTemps;
|
||||
if (totalCotisationsToutTemps == 0 && cotSynthese != null) {
|
||||
// totalPayeAnnee = montant payé sur l'année en cours (meilleure approximation disponible)
|
||||
final totalPayeAnnee = _toDouble(cotSynthese['totalPayeAnnee']);
|
||||
if (totalPayeAnnee > 0) totalCotisationsToutTemps = totalPayeAnnee;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// MON SOLDE TOTAL = cotisations payées + épargne
|
||||
// ------------------------------------------------------------------
|
||||
final monSoldeTotal = totalCotisationsToutTemps + s.monSoldeEpargne;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Taux d'engagement (en %)
|
||||
// Priorité : tauxParticipationPerso > tauxCotisationsPerso > calculé depuis cotSynthese
|
||||
// ------------------------------------------------------------------
|
||||
int? tauxBrut = s.tauxParticipationPerso ?? s.tauxCotisationsPerso;
|
||||
double engagementRate = (tauxBrut ?? 0) / 100.0;
|
||||
if (engagementRate == 0 && cotSynthese != null) {
|
||||
final montantDu = _toDouble(cotSynthese['montantDu']);
|
||||
final totalPayeAnnee = _toDouble(cotSynthese['totalPayeAnnee']);
|
||||
final total = montantDu + totalPayeAnnee;
|
||||
if (total > 0) engagementRate = totalPayeAnnee / total;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Nombre de cotisations — utilize NEW nombreCotisationsTotal if available
|
||||
// ------------------------------------------------------------------
|
||||
final int nombreCotisations = s.nombreCotisationsTotal > 0
|
||||
? s.nombreCotisationsTotal
|
||||
: s.nombreCotisationsPayees;
|
||||
|
||||
final stats = DashboardStatsEntity(
|
||||
totalMembers: 0,
|
||||
activeMembers: 0,
|
||||
totalEvents: 0,
|
||||
upcomingEvents: s.evenementsAVenir,
|
||||
totalContributions: s.nombreCotisationsPayees,
|
||||
totalContributions: nombreCotisations,
|
||||
totalContributionAmount: monSoldeTotal,
|
||||
contributionsAmountOnly: totalCotisationsToutTemps,
|
||||
pendingRequests: 0,
|
||||
completedProjects: 0,
|
||||
monthlyGrowth: s.evolutionEpargneNombre,
|
||||
engagementRate: ((s.tauxParticipationPerso ?? s.tauxCotisationsPerso) ?? 0) / 100.0,
|
||||
engagementRate: engagementRate,
|
||||
lastUpdated: now,
|
||||
totalOrganizations: null,
|
||||
organizationTypeDistribution: null,
|
||||
@@ -69,10 +150,20 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
stats: stats,
|
||||
recentActivities: const [],
|
||||
upcomingEvents: const [],
|
||||
userPreferences: <String, dynamic>{},
|
||||
userPreferences: const <String, dynamic>{},
|
||||
organizationId: '',
|
||||
userId: userId,
|
||||
monCompte: compteModel != null ? _mapCompteToEntity(compteModel) : null,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
static double _toDouble(dynamic v) {
|
||||
if (v == null) return 0;
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -142,6 +233,28 @@ class DashboardRepositoryImpl implements DashboardRepository {
|
||||
}
|
||||
}
|
||||
|
||||
CompteAdherentEntity _mapCompteToEntity(CompteAdherentModel model) {
|
||||
return CompteAdherentEntity(
|
||||
numeroMembre: model.numeroMembre,
|
||||
nomComplet: model.nomComplet,
|
||||
organisationNom: model.organisationNom,
|
||||
dateAdhesion: model.dateAdhesion != null ? DateTime.tryParse(model.dateAdhesion!) : null,
|
||||
statutCompte: model.statutCompte,
|
||||
soldeCotisations: model.soldeCotisations,
|
||||
soldeEpargne: model.soldeEpargne,
|
||||
soldeBloque: model.soldeBloque,
|
||||
soldeTotalDisponible: model.soldeTotalDisponible,
|
||||
encoursCreditTotal: model.encoursCreditTotal,
|
||||
capaciteEmprunt: model.capaciteEmprunt,
|
||||
nombreCotisationsPayees: model.nombreCotisationsPayees,
|
||||
nombreCotisationsTotal: model.nombreCotisationsTotal,
|
||||
nombreCotisationsEnRetard: model.nombreCotisationsEnRetard,
|
||||
engagementRate: (model.tauxEngagement ?? 0) / 100.0,
|
||||
nombreComptesEpargne: model.nombreComptesEpargne,
|
||||
dateCalcul: DateTime.tryParse(model.dateCalcul) ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
// Mappers
|
||||
DashboardEntity _mapToEntity(DashboardDataModel model) {
|
||||
return DashboardEntity(
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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 '../../presentation/bloc/finance_state.dart';
|
||||
|
||||
/// Repository pour les données financières (cotisations, synthèse).
|
||||
/// Appelle les endpoints /api/cotisations/mes-cotisations/*.
|
||||
@lazySingleton
|
||||
class FinanceRepository {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
FinanceRepository(this._apiClient);
|
||||
|
||||
/// Synthèse des cotisations du membre connecté (GET /api/cotisations/mes-cotisations/synthese).
|
||||
Future<FinanceSummary> getFinancialSummary() async {
|
||||
try {
|
||||
final response = await _apiClient.get('/api/cotisations/mes-cotisations/synthese');
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final totalPayeAnnee = (data['totalPayeAnnee'] is num)
|
||||
? (data['totalPayeAnnee'] as num).toDouble()
|
||||
: 0.0;
|
||||
final montantDu = (data['montantDu'] is num)
|
||||
? (data['montantDu'] as num).toDouble()
|
||||
: 0.0;
|
||||
final epargneBalance = (data['epargneBalance'] is num)
|
||||
? (data['epargneBalance'] as num).toDouble()
|
||||
: 0.0;
|
||||
return FinanceSummary(
|
||||
totalContributionsPaid: totalPayeAnnee,
|
||||
totalContributionsPending: montantDu,
|
||||
epargneBalance: epargneBalance,
|
||||
);
|
||||
} on DioException catch (e, st) {
|
||||
AppLogger.error('FinanceRepository: getFinancialSummary échoué', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
AppLogger.error('FinanceRepository: getFinancialSummary erreur inattendue', error: e, stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cotisations en attente du membre connecté (GET /api/cotisations/mes-cotisations/en-attente).
|
||||
Future<List<FinanceTransaction>> getTransactions() async {
|
||||
try {
|
||||
final response = await _apiClient.get('/api/cotisations/mes-cotisations/en-attente');
|
||||
final List<dynamic> data = response.data is List ? response.data as List : [];
|
||||
return data
|
||||
.map((json) => _transactionFromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on DioException catch (e, st) {
|
||||
AppLogger.error('FinanceRepository: getTransactions échoué', error: e, stackTrace: st);
|
||||
if (e.response?.statusCode == 404) return [];
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static FinanceTransaction _transactionFromJson(Map<String, dynamic> json) {
|
||||
final id = json['id']?.toString() ?? '';
|
||||
final ref = json['numeroReference']?.toString() ?? '';
|
||||
final nomMembre = json['nomMembre']?.toString() ?? 'Cotisation';
|
||||
final montantDu = (json['montantDu'] is num)
|
||||
? (json['montantDu'] as num).toDouble()
|
||||
: 0.0;
|
||||
final statutLibelle = json['statutLibelle']?.toString() ?? 'En attente';
|
||||
final dateEcheance = json['dateEcheance']?.toString();
|
||||
final dateStr = dateEcheance != null
|
||||
? _parseDateToDisplay(dateEcheance)
|
||||
: '';
|
||||
return FinanceTransaction(
|
||||
id: id,
|
||||
title: nomMembre.isNotEmpty ? nomMembre : 'Cotisation $ref',
|
||||
date: dateStr,
|
||||
amount: montantDu,
|
||||
status: statutLibelle,
|
||||
);
|
||||
}
|
||||
|
||||
static String _parseDateToDisplay(String isoDate) {
|
||||
try {
|
||||
final d = DateTime.parse(isoDate);
|
||||
return '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year}';
|
||||
} catch (e) {
|
||||
AppLogger.warning('FinanceRepository: _parseDateToDisplay date invalide', tag: isoDate);
|
||||
return isoDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import '../models/dashboard_stats_model.dart';
|
||||
import '../cache/dashboard_cache_manager.dart';
|
||||
import '../../../../core/storage/dashboard_cache_manager.dart';
|
||||
|
||||
/// Service de mode hors ligne avec synchronisation pour le Dashboard
|
||||
class DashboardOfflineService {
|
||||
@@ -14,7 +15,7 @@ class DashboardOfflineService {
|
||||
static const String _offlineModeKey = 'dashboard_offline_mode';
|
||||
|
||||
final DashboardCacheManager _cacheManager;
|
||||
final Dio _dio;
|
||||
final ApiClient _apiClient;
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
@@ -35,7 +36,7 @@ class DashboardOfflineService {
|
||||
Stream<OfflineStatus> get statusStream => _statusController.stream;
|
||||
Stream<SyncProgress> get syncStream => _syncController.stream;
|
||||
|
||||
DashboardOfflineService(this._cacheManager, this._dio);
|
||||
DashboardOfflineService(this._cacheManager, this._apiClient);
|
||||
|
||||
/// Initialise le service hors ligne
|
||||
Future<void> initialize() async {
|
||||
@@ -216,14 +217,13 @@ class DashboardOfflineService {
|
||||
final userId = action.data['userId'] as String?;
|
||||
if (orgId == null || userId == null) return;
|
||||
|
||||
final response = await _dio.get('/api/dashboard/stats', queryParameters: {
|
||||
final response = await _apiClient.get('/api/dashboard/stats', queryParameters: {
|
||||
'organisationId': orgId,
|
||||
});
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
await _cacheManager.cacheDashboardData(
|
||||
DashboardDataModel.fromJson(response.data as Map<String, dynamic>),
|
||||
orgId,
|
||||
userId,
|
||||
await _cacheManager.setKey(
|
||||
'dashboard_${orgId}_$userId',
|
||||
response.data as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -234,7 +234,7 @@ class DashboardOfflineService {
|
||||
final preferences = action.data['preferences'] as Map<String, dynamic>?;
|
||||
if (userId == null || preferences == null) return;
|
||||
|
||||
await _dio.put('/api/membres/$userId/preferences', data: preferences);
|
||||
await _apiClient.put('/api/membres/$userId/preferences', data: preferences);
|
||||
}
|
||||
|
||||
/// Synchronise le marquage d'activité comme lue
|
||||
@@ -242,7 +242,7 @@ class DashboardOfflineService {
|
||||
final activityId = action.data['activityId'] as String?;
|
||||
if (activityId == null) return;
|
||||
|
||||
await _dio.put('/api/notifications/$activityId/read');
|
||||
await _apiClient.put('/api/notifications/$activityId/read');
|
||||
}
|
||||
|
||||
/// Synchronise l'inscription à un événement
|
||||
@@ -251,7 +251,7 @@ class DashboardOfflineService {
|
||||
final membreId = action.data['membreId'] as String?;
|
||||
if (eventId == null || membreId == null) return;
|
||||
|
||||
await _dio.post('/api/evenements/$eventId/inscription', data: {
|
||||
await _apiClient.post('/api/evenements/$eventId/inscription', data: {
|
||||
'membreId': membreId,
|
||||
});
|
||||
}
|
||||
@@ -262,7 +262,7 @@ class DashboardOfflineService {
|
||||
final params = action.data['params'] as Map<String, dynamic>?;
|
||||
if (reportType == null) return;
|
||||
|
||||
await _dio.post('/api/export/$reportType', data: params ?? {});
|
||||
await _apiClient.post('/api/export/$reportType', data: params ?? {});
|
||||
}
|
||||
|
||||
/// Sauvegarde les actions en attente
|
||||
@@ -315,7 +315,7 @@ class DashboardOfflineService {
|
||||
}
|
||||
|
||||
/// Force une synchronisation manuelle
|
||||
Future<void> forcSync() async {
|
||||
Future<void> forceSync() async {
|
||||
if (!_isOnline) {
|
||||
throw Exception('Impossible de synchroniser hors ligne');
|
||||
}
|
||||
@@ -328,7 +328,8 @@ class DashboardOfflineService {
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
return await _cacheManager.getCachedDashboardData(organizationId, userId);
|
||||
final m = _cacheManager.getKey<Map<String, dynamic>>('dashboard_${organizationId}_$userId');
|
||||
return m != null ? DashboardDataModel.fromJson(m) : null;
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles hors ligne
|
||||
|
||||
@@ -19,7 +19,8 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
bool _isMonitoring = false;
|
||||
DateTime _startTime = DateTime.now();
|
||||
|
||||
int _alertsGeneratedCount = 0;
|
||||
|
||||
// Seuils d'alerte configurables
|
||||
final double _memoryThreshold = DashboardConfig.getAlertThreshold('memoryUsage');
|
||||
final double _cpuThreshold = DashboardConfig.getAlertThreshold('cpuUsage');
|
||||
@@ -147,18 +148,16 @@ class DashboardPerformanceMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient la latence réseau
|
||||
/// Obtient la latence réseau (hôte/port depuis DashboardConfig.apiBaseUrl).
|
||||
Future<int> _getNetworkLatency() async {
|
||||
try {
|
||||
final uri = Uri.parse(DashboardConfig.apiBaseUrl);
|
||||
final host = uri.host.isNotEmpty ? uri.host : 'localhost';
|
||||
final port = uri.hasPort ? uri.port : 8085;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// Ping vers le serveur de l'API
|
||||
final socket = await Socket.connect('localhost', 8080)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
|
||||
final socket = await Socket.connect(host, port).timeout(const Duration(seconds: 5));
|
||||
stopwatch.stop();
|
||||
await socket.close();
|
||||
|
||||
return stopwatch.elapsedMilliseconds;
|
||||
} catch (e) {
|
||||
return _simulateNetworkLatency();
|
||||
@@ -228,6 +227,7 @@ class DashboardPerformanceMonitor {
|
||||
void _checkAlerts(PerformanceMetrics metrics) {
|
||||
// Alerte mémoire
|
||||
if (metrics.memoryUsage > _memoryThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.memory,
|
||||
severity: AlertSeverity.warning,
|
||||
@@ -240,6 +240,7 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
// Alerte CPU
|
||||
if (metrics.cpuUsage > _cpuThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.cpu,
|
||||
severity: AlertSeverity.warning,
|
||||
@@ -252,6 +253,7 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
// Alerte latence réseau
|
||||
if (metrics.networkLatency > _networkLatencyThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.network,
|
||||
severity: AlertSeverity.error,
|
||||
@@ -264,6 +266,7 @@ class DashboardPerformanceMonitor {
|
||||
|
||||
// Alerte frame rate
|
||||
if (metrics.frameRate < _frameRateThreshold) {
|
||||
_alertsGeneratedCount++;
|
||||
_alertController.add(PerformanceAlert(
|
||||
type: AlertType.performance,
|
||||
severity: AlertSeverity.warning,
|
||||
@@ -298,8 +301,7 @@ class DashboardPerformanceMonitor {
|
||||
if (_snapshots.isEmpty) {
|
||||
return PerformanceStats.empty();
|
||||
}
|
||||
|
||||
return PerformanceStats.fromSnapshots(_snapshots);
|
||||
return PerformanceStats.fromSnapshots(_snapshots, alertsGenerated: _alertsGeneratedCount);
|
||||
}
|
||||
|
||||
/// Méthodes de simulation pour le développement
|
||||
@@ -508,7 +510,7 @@ class PerformanceStats {
|
||||
);
|
||||
}
|
||||
|
||||
factory PerformanceStats.fromSnapshots(List<PerformanceSnapshot> snapshots) {
|
||||
factory PerformanceStats.fromSnapshots(List<PerformanceSnapshot> snapshots, {int alertsGenerated = 0}) {
|
||||
if (snapshots.isEmpty) return PerformanceStats.empty();
|
||||
|
||||
final metrics = snapshots.map((s) => s.metrics).toList();
|
||||
@@ -520,7 +522,7 @@ class PerformanceStats {
|
||||
peakMemoryUsage: metrics.map((m) => m.memoryUsage).reduce((a, b) => a > b ? a : b),
|
||||
averageCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a + b) / metrics.length,
|
||||
peakCpuUsage: metrics.map((m) => m.cpuUsage).reduce((a, b) => a > b ? a : b),
|
||||
alertsGenerated: 0, // À implémenter si nécessaire
|
||||
alertsGenerated: alertsGenerated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user