472 lines
14 KiB
Dart
472 lines
14 KiB
Dart
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';
|
|
}
|
|
}
|