Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
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';
|
||||
|
||||
/// Service de mode hors ligne avec synchronisation pour le Dashboard
|
||||
class DashboardOfflineService {
|
||||
static const String _offlineQueueKey = 'dashboard_offline_queue';
|
||||
static const String _lastSyncKey = 'dashboard_last_sync';
|
||||
static const String _offlineModeKey = 'dashboard_offline_mode';
|
||||
|
||||
final DashboardCacheManager _cacheManager;
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||
Timer? _syncTimer;
|
||||
|
||||
final StreamController<OfflineStatus> _statusController =
|
||||
StreamController<OfflineStatus>.broadcast();
|
||||
final StreamController<SyncProgress> _syncController =
|
||||
StreamController<SyncProgress>.broadcast();
|
||||
|
||||
final List<OfflineAction> _pendingActions = [];
|
||||
bool _isOnline = true;
|
||||
bool _isSyncing = false;
|
||||
DateTime? _lastSyncTime;
|
||||
|
||||
// Streams publics
|
||||
Stream<OfflineStatus> get statusStream => _statusController.stream;
|
||||
Stream<SyncProgress> get syncStream => _syncController.stream;
|
||||
|
||||
DashboardOfflineService(this._cacheManager);
|
||||
|
||||
/// Initialise le service hors ligne
|
||||
Future<void> initialize() async {
|
||||
debugPrint('📱 Initialisation du service hors ligne...');
|
||||
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Charger les actions en attente
|
||||
await _loadPendingActions();
|
||||
|
||||
// Charger la dernière synchronisation
|
||||
_loadLastSyncTime();
|
||||
|
||||
// Vérifier la connectivité initiale
|
||||
final connectivityResult = await _connectivity.checkConnectivity();
|
||||
_updateConnectivityStatus(connectivityResult);
|
||||
|
||||
// Écouter les changements de connectivité
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
|
||||
(List<ConnectivityResult> results) => _updateConnectivityStatus(results),
|
||||
);
|
||||
|
||||
// Démarrer la synchronisation automatique
|
||||
_startAutoSync();
|
||||
|
||||
debugPrint('✅ Service hors ligne initialisé');
|
||||
}
|
||||
|
||||
/// Met à jour le statut de connectivité
|
||||
void _updateConnectivityStatus(dynamic result) {
|
||||
final wasOnline = _isOnline;
|
||||
if (result is List<ConnectivityResult>) {
|
||||
_isOnline = result.any((r) => r != ConnectivityResult.none);
|
||||
} else if (result is ConnectivityResult) {
|
||||
_isOnline = result != ConnectivityResult.none;
|
||||
} else {
|
||||
_isOnline = false;
|
||||
}
|
||||
|
||||
debugPrint('🌐 Connectivité: ${_isOnline ? 'En ligne' : 'Hors ligne'}');
|
||||
|
||||
_statusController.add(OfflineStatus(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
));
|
||||
|
||||
// Si on revient en ligne, synchroniser
|
||||
if (!wasOnline && _isOnline && _pendingActions.isNotEmpty) {
|
||||
_syncPendingActions();
|
||||
}
|
||||
}
|
||||
|
||||
/// Démarre la synchronisation automatique
|
||||
void _startAutoSync() {
|
||||
_syncTimer = Timer.periodic(
|
||||
const Duration(minutes: 5),
|
||||
(_) {
|
||||
if (_isOnline && _pendingActions.isNotEmpty) {
|
||||
_syncPendingActions();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Ajoute une action à la queue hors ligne
|
||||
Future<void> queueAction(OfflineAction action) async {
|
||||
_pendingActions.add(action);
|
||||
await _savePendingActions();
|
||||
|
||||
debugPrint('📝 Action mise en queue: ${action.type} (${_pendingActions.length} en attente)');
|
||||
|
||||
_statusController.add(OfflineStatus(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
));
|
||||
|
||||
// Si en ligne, essayer de synchroniser immédiatement
|
||||
if (_isOnline) {
|
||||
_syncPendingActions();
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronise les actions en attente
|
||||
Future<void> _syncPendingActions() async {
|
||||
if (_isSyncing || _pendingActions.isEmpty || !_isOnline) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isSyncing = true;
|
||||
debugPrint('🔄 Début de la synchronisation (${_pendingActions.length} actions)');
|
||||
|
||||
_syncController.add(SyncProgress(
|
||||
isActive: true,
|
||||
totalActions: _pendingActions.length,
|
||||
completedActions: 0,
|
||||
currentAction: _pendingActions.first.type.toString(),
|
||||
));
|
||||
|
||||
final actionsToSync = List<OfflineAction>.from(_pendingActions);
|
||||
int completedCount = 0;
|
||||
|
||||
for (final action in actionsToSync) {
|
||||
try {
|
||||
await _executeAction(action);
|
||||
_pendingActions.remove(action);
|
||||
completedCount++;
|
||||
|
||||
_syncController.add(SyncProgress(
|
||||
isActive: true,
|
||||
totalActions: actionsToSync.length,
|
||||
completedActions: completedCount,
|
||||
currentAction: completedCount < actionsToSync.length
|
||||
? actionsToSync[completedCount].type.toString()
|
||||
: null,
|
||||
));
|
||||
|
||||
debugPrint('✅ Action synchronisée: ${action.type}');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la synchronisation de ${action.type}: $e');
|
||||
|
||||
// Marquer l'action comme échouée si trop de tentatives
|
||||
action.retryCount++;
|
||||
if (action.retryCount >= 3) {
|
||||
_pendingActions.remove(action);
|
||||
debugPrint('🗑️ Action abandonnée après 3 tentatives: ${action.type}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _savePendingActions();
|
||||
_lastSyncTime = DateTime.now();
|
||||
await _saveLastSyncTime();
|
||||
|
||||
_syncController.add(SyncProgress(
|
||||
isActive: false,
|
||||
totalActions: actionsToSync.length,
|
||||
completedActions: completedCount,
|
||||
currentAction: null,
|
||||
));
|
||||
|
||||
_statusController.add(OfflineStatus(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
));
|
||||
|
||||
_isSyncing = false;
|
||||
debugPrint('✅ Synchronisation terminée ($completedCount/${actionsToSync.length} réussies)');
|
||||
}
|
||||
|
||||
/// Exécute une action spécifique
|
||||
Future<void> _executeAction(OfflineAction action) async {
|
||||
switch (action.type) {
|
||||
case OfflineActionType.refreshDashboard:
|
||||
await _syncDashboardData(action);
|
||||
break;
|
||||
case OfflineActionType.updatePreferences:
|
||||
await _syncUserPreferences(action);
|
||||
break;
|
||||
case OfflineActionType.markActivityRead:
|
||||
await _syncActivityRead(action);
|
||||
break;
|
||||
case OfflineActionType.joinEvent:
|
||||
await _syncEventJoin(action);
|
||||
break;
|
||||
case OfflineActionType.exportReport:
|
||||
await _syncReportExport(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronise les données du dashboard
|
||||
Future<void> _syncDashboardData(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation des données
|
||||
await Future.delayed(const Duration(milliseconds: 500)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise les préférences utilisateur
|
||||
Future<void> _syncUserPreferences(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation des préférences
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise le marquage d'activité comme lue
|
||||
Future<void> _syncActivityRead(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation du marquage
|
||||
await Future.delayed(const Duration(milliseconds: 200)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise l'inscription à un événement
|
||||
Future<void> _syncEventJoin(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation d'inscription
|
||||
await Future.delayed(const Duration(milliseconds: 400)); // Simulation
|
||||
}
|
||||
|
||||
/// Synchronise l'export de rapport
|
||||
Future<void> _syncReportExport(OfflineAction action) async {
|
||||
// TODO: Implémenter la synchronisation d'export
|
||||
await Future.delayed(const Duration(milliseconds: 800)); // Simulation
|
||||
}
|
||||
|
||||
/// Sauvegarde les actions en attente
|
||||
Future<void> _savePendingActions() async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final actionsJson = _pendingActions
|
||||
.map((action) => action.toJson())
|
||||
.toList();
|
||||
|
||||
await _prefs!.setString(_offlineQueueKey, jsonEncode(actionsJson));
|
||||
}
|
||||
|
||||
/// Charge les actions en attente
|
||||
Future<void> _loadPendingActions() async {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final actionsJsonString = _prefs!.getString(_offlineQueueKey);
|
||||
if (actionsJsonString != null) {
|
||||
try {
|
||||
final actionsJson = jsonDecode(actionsJsonString) as List;
|
||||
_pendingActions.clear();
|
||||
_pendingActions.addAll(
|
||||
actionsJson.map((json) => OfflineAction.fromJson(json)),
|
||||
);
|
||||
|
||||
debugPrint('📋 ${_pendingActions.length} actions chargées depuis le cache');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du chargement des actions: $e');
|
||||
await _prefs!.remove(_offlineQueueKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde l'heure de dernière synchronisation
|
||||
Future<void> _saveLastSyncTime() async {
|
||||
if (_prefs == null || _lastSyncTime == null) return;
|
||||
|
||||
await _prefs!.setInt(_lastSyncKey, _lastSyncTime!.millisecondsSinceEpoch);
|
||||
}
|
||||
|
||||
/// Charge l'heure de dernière synchronisation
|
||||
void _loadLastSyncTime() {
|
||||
if (_prefs == null) return;
|
||||
|
||||
final lastSyncMs = _prefs!.getInt(_lastSyncKey);
|
||||
if (lastSyncMs != null) {
|
||||
_lastSyncTime = DateTime.fromMillisecondsSinceEpoch(lastSyncMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Force une synchronisation manuelle
|
||||
Future<void> forcSync() async {
|
||||
if (!_isOnline) {
|
||||
throw Exception('Impossible de synchroniser hors ligne');
|
||||
}
|
||||
|
||||
await _syncPendingActions();
|
||||
}
|
||||
|
||||
/// Obtient les données en mode hors ligne
|
||||
Future<DashboardDataModel?> getOfflineData(
|
||||
String organizationId,
|
||||
String userId,
|
||||
) async {
|
||||
return await _cacheManager.getCachedDashboardData(organizationId, userId);
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles hors ligne
|
||||
Future<bool> hasOfflineData(String organizationId, String userId) async {
|
||||
final data = await getOfflineData(organizationId, userId);
|
||||
return data != null;
|
||||
}
|
||||
|
||||
/// Obtient les statistiques du mode hors ligne
|
||||
OfflineStats getStats() {
|
||||
return OfflineStats(
|
||||
isOnline: _isOnline,
|
||||
pendingActionsCount: _pendingActions.length,
|
||||
lastSyncTime: _lastSyncTime,
|
||||
isSyncing: _isSyncing,
|
||||
cacheStats: _cacheManager.getCacheStats(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Nettoie les anciennes actions
|
||||
Future<void> cleanupOldActions() async {
|
||||
final cutoffTime = DateTime.now().subtract(const Duration(days: 7));
|
||||
|
||||
_pendingActions.removeWhere((action) =>
|
||||
action.timestamp.isBefore(cutoffTime));
|
||||
|
||||
await _savePendingActions();
|
||||
}
|
||||
|
||||
/// Libère les ressources
|
||||
void dispose() {
|
||||
_connectivitySubscription?.cancel();
|
||||
_syncTimer?.cancel();
|
||||
_statusController.close();
|
||||
_syncController.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Action hors ligne
|
||||
class OfflineAction {
|
||||
final String id;
|
||||
final OfflineActionType type;
|
||||
final Map<String, dynamic> data;
|
||||
final DateTime timestamp;
|
||||
int retryCount;
|
||||
|
||||
OfflineAction({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.data,
|
||||
required this.timestamp,
|
||||
this.retryCount = 0,
|
||||
});
|
||||
|
||||
factory OfflineAction.fromJson(Map<String, dynamic> json) {
|
||||
return OfflineAction(
|
||||
id: json['id'] as String,
|
||||
type: OfflineActionType.values.firstWhere(
|
||||
(t) => t.name == json['type'],
|
||||
),
|
||||
data: json['data'] as Map<String, dynamic>,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
retryCount: json['retryCount'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type.name,
|
||||
'data': data,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'retryCount': retryCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Types d'actions hors ligne
|
||||
enum OfflineActionType {
|
||||
refreshDashboard,
|
||||
updatePreferences,
|
||||
markActivityRead,
|
||||
joinEvent,
|
||||
exportReport,
|
||||
}
|
||||
|
||||
/// Statut hors ligne
|
||||
class OfflineStatus {
|
||||
final bool isOnline;
|
||||
final int pendingActionsCount;
|
||||
final DateTime? lastSyncTime;
|
||||
|
||||
const OfflineStatus({
|
||||
required this.isOnline,
|
||||
required this.pendingActionsCount,
|
||||
this.lastSyncTime,
|
||||
});
|
||||
|
||||
String get statusText {
|
||||
if (isOnline) {
|
||||
if (pendingActionsCount > 0) {
|
||||
return 'En ligne - $pendingActionsCount actions en attente';
|
||||
} else {
|
||||
return 'En ligne - Synchronisé';
|
||||
}
|
||||
} else {
|
||||
return 'Hors ligne - Mode cache activé';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Progression de synchronisation
|
||||
class SyncProgress {
|
||||
final bool isActive;
|
||||
final int totalActions;
|
||||
final int completedActions;
|
||||
final String? currentAction;
|
||||
|
||||
const SyncProgress({
|
||||
required this.isActive,
|
||||
required this.totalActions,
|
||||
required this.completedActions,
|
||||
this.currentAction,
|
||||
});
|
||||
|
||||
double get progress {
|
||||
if (totalActions == 0) return 1.0;
|
||||
return completedActions / totalActions;
|
||||
}
|
||||
|
||||
String get progressText {
|
||||
if (!isActive) return 'Synchronisation terminée';
|
||||
if (currentAction != null) {
|
||||
return 'Synchronisation: $currentAction ($completedActions/$totalActions)';
|
||||
}
|
||||
return 'Synchronisation en cours... ($completedActions/$totalActions)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques du mode hors ligne
|
||||
class OfflineStats {
|
||||
final bool isOnline;
|
||||
final int pendingActionsCount;
|
||||
final DateTime? lastSyncTime;
|
||||
final bool isSyncing;
|
||||
final Map<String, dynamic> cacheStats;
|
||||
|
||||
const OfflineStats({
|
||||
required this.isOnline,
|
||||
required this.pendingActionsCount,
|
||||
this.lastSyncTime,
|
||||
required this.isSyncing,
|
||||
required this.cacheStats,
|
||||
});
|
||||
|
||||
String get lastSyncText {
|
||||
if (lastSyncTime == null) return 'Jamais synchronisé';
|
||||
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(lastSyncTime!);
|
||||
|
||||
if (diff.inMinutes < 1) return 'Synchronisé à l\'instant';
|
||||
if (diff.inMinutes < 60) return 'Synchronisé il y a ${diff.inMinutes}min';
|
||||
if (diff.inHours < 24) return 'Synchronisé il y a ${diff.inHours}h';
|
||||
return 'Synchronisé il y a ${diff.inDays}j';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user