Initial upload: fin65-mobile
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
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<Account> accounts;
|
||||
final List<Category> categories;
|
||||
final List<TransactionItem> 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<List<PendingOperation>> loadPendingOperations() {
|
||||
return store.loadPendingOperations();
|
||||
}
|
||||
|
||||
Future<String> getApiUrl() {
|
||||
return apiService.getBaseUrl();
|
||||
}
|
||||
|
||||
Future<bool> testBackend() async {
|
||||
try {
|
||||
return await apiService.healthCheck();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<SyncSnapshot> 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<SyncSnapshot> 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<bool> createTransactionSmart({
|
||||
required String label,
|
||||
required double amount,
|
||||
required String type,
|
||||
required int accountId,
|
||||
int? targetAccountId,
|
||||
int? categoryId,
|
||||
String? note,
|
||||
}) async {
|
||||
final payload = <String, dynamic>{
|
||||
'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<void> _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<Account> accounts,
|
||||
required List<Category> categories,
|
||||
required List<TransactionItem> transactions,
|
||||
required List<PendingOperation> pending,
|
||||
required DateTime? lastSyncAt,
|
||||
required bool isOnline,
|
||||
String? message,
|
||||
}) {
|
||||
final accountMap = <int, Account>{
|
||||
for (final account in accounts) account.id: account,
|
||||
};
|
||||
|
||||
var adjustedDashboard = dashboard;
|
||||
final pendingTransactions = <TransactionItem>[];
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user