import '../config/app_config.dart'; import '../models/account.dart'; import '../models/category.dart'; import '../models/dashboard.dart'; import '../models/pending_operation.dart'; import '../models/transaction_item.dart'; import 'api_service.dart'; import 'offline_store.dart'; class SyncSnapshot { final DashboardData? dashboard; final List accounts; final List categories; final List transactions; final int pendingCount; final DateTime? lastSyncAt; final bool isOnline; final String? message; SyncSnapshot({ required this.dashboard, required this.accounts, required this.categories, required this.transactions, required this.pendingCount, required this.lastSyncAt, required this.isOnline, this.message, }); } class SyncService { SyncService({ ApiService? apiService, OfflineStore? offlineStore, }) : store = offlineStore ?? OfflineStore.instance, apiService = apiService ?? ApiService(store: offlineStore ?? OfflineStore.instance); final OfflineStore store; final ApiService apiService; Future> loadPendingOperations() { return store.loadPendingOperations(); } Future getApiUrl() { return apiService.getBaseUrl(); } Future testBackend() async { try { return await apiService.healthCheck(); } catch (_) { return false; } } Future loadLocalSnapshot({bool isOnline = false, String? message}) async { final dashboard = await store.loadDashboard(); final accounts = await store.loadAccounts(); final categories = await store.loadCategories(); final transactions = await store.loadTransactions(limit: AppConfig.defaultTransactionLimit); final pending = await store.loadPendingOperations(); final lastSyncAt = await store.getLastSyncAt(); return _applyPendingEstimates( dashboard: dashboard, accounts: accounts, categories: categories, transactions: transactions, pending: pending, lastSyncAt: lastSyncAt, isOnline: isOnline, message: message, ); } Future synchronize() async { try { await apiService.healthCheck(); await _syncPendingOperations(); final accounts = await apiService.getAccounts(); final categories = await apiService.getCategories(); final dashboard = await apiService.getDashboard(); final transactions = await apiService.getTransactions(limit: AppConfig.defaultTransactionLimit); await store.saveServerSnapshot( dashboard: dashboard, accounts: accounts, categories: categories, transactions: transactions, ); return loadLocalSnapshot(isOnline: true, message: null); } catch (e) { return loadLocalSnapshot( isOnline: false, message: 'Backend indisponible. Données locales affichées.', ); } } Future createTransactionSmart({ required String label, required double amount, required String type, required int accountId, int? targetAccountId, int? categoryId, String? note, }) async { final payload = { 'date': DateTime.now().toIso8601String(), 'label': label, 'amount': amount, 'type': type, 'account_id': accountId, 'target_account_id': type == 'transfer' ? targetAccountId : null, 'category_id': type == 'transfer' ? null : categoryId, 'created_from': 'android', 'note': note, }; try { await apiService.healthCheck(); await apiService.createTransactionFromPayload(payload); await synchronize(); return true; } catch (_) { // Offline or backend unreachable: the operation is persisted locally in // SQLite. The UI can then immediately reload an estimated snapshot. await store.enqueueCreateTransaction(payload); return false; } } Future _syncPendingOperations() async { final pending = await store.loadPendingOperations(); for (final operation in pending) { try { if (operation.operationType == 'create_transaction') { await apiService.createTransactionFromPayload(operation.payload); await store.deletePendingOperation(operation.id); } } catch (e) { await store.markPendingError(operation.id, e.toString()); rethrow; } } } SyncSnapshot _applyPendingEstimates({ required DashboardData? dashboard, required List accounts, required List categories, required List transactions, required List pending, required DateTime? lastSyncAt, required bool isOnline, String? message, }) { final accountMap = { for (final account in accounts) account.id: account, }; var adjustedDashboard = dashboard; final pendingTransactions = []; for (final operation in pending) { if (operation.operationType != 'create_transaction') continue; final item = TransactionItem.fromPending( localOperationId: operation.id, payload: operation.payload, ); pendingTransactions.add(item); final amount = item.amount; final source = accountMap[item.accountId]; if (item.type == 'expense') { if (source != null) { accountMap[item.accountId] = source.copyWith( appBalance: source.appBalance - amount, ); } adjustedDashboard = _adjustDashboard( adjustedDashboard, totalDelta: -amount, availableDelta: -amount, incomeDelta: 0, expenseDelta: amount, monthlyBalanceDelta: -amount, ); } else if (item.type == 'income') { if (source != null) { accountMap[item.accountId] = source.copyWith( appBalance: source.appBalance + amount, ); } adjustedDashboard = _adjustDashboard( adjustedDashboard, totalDelta: amount, availableDelta: amount, incomeDelta: amount, expenseDelta: 0, monthlyBalanceDelta: amount, ); } else if (item.type == 'transfer') { if (source != null) { accountMap[item.accountId] = source.copyWith( appBalance: source.appBalance - amount, ); } final targetId = item.targetAccountId; if (targetId != null) { final target = accountMap[targetId]; if (target != null) { accountMap[targetId] = target.copyWith( appBalance: target.appBalance + amount, ); } } } } final adjustedAccounts = accountMap.values.toList() ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); final allTransactions = [ ...pendingTransactions.reversed, ...transactions, ]; return SyncSnapshot( dashboard: adjustedDashboard, accounts: adjustedAccounts, categories: categories, transactions: allTransactions, pendingCount: pending.length, lastSyncAt: lastSyncAt, isOnline: isOnline, message: message, ); } DashboardData? _adjustDashboard( DashboardData? dashboard, { required double totalDelta, required double availableDelta, required double incomeDelta, required double expenseDelta, required double monthlyBalanceDelta, }) { if (dashboard == null) return null; return dashboard.copyWith( totalBalance: dashboard.totalBalance + totalDelta, availableAfterReserves: dashboard.availableAfterReserves + availableDelta, monthlyIncome: dashboard.monthlyIncome + incomeDelta, monthlyExpenses: dashboard.monthlyExpenses + expenseDelta, monthlyBalance: dashboard.monthlyBalance + monthlyBalanceDelta, ); } }