import 'dart:convert'; import 'package:path/path.dart' as p; import 'package:sqflite/sqflite.dart'; import '../models/account.dart'; import '../models/category.dart'; import '../models/dashboard.dart'; import '../models/pending_operation.dart'; import '../models/transaction_item.dart'; class OfflineStore { OfflineStore._(); static final OfflineStore instance = OfflineStore._(); Database? _database; Future get database async { final existing = _database; if (existing != null) return existing; final dbPath = await getDatabasesPath(); final path = p.join(dbPath, 'fin65_mobile.db'); final db = await openDatabase( path, version: 1, onCreate: _createSchema, ); _database = db; return db; } Future _createSchema(Database db, int version) async { await db.execute(''' CREATE TABLE settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE sync_state ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE accounts_cache ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL, currency TEXT NOT NULL, initial_balance REAL NOT NULL, app_balance REAL NOT NULL, is_active INTEGER NOT NULL, updated_at TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE categories_cache ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL, parent_id INTEGER, updated_at TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE transactions_cache ( id INTEGER PRIMARY KEY, label TEXT NOT NULL, amount REAL NOT NULL, type TEXT NOT NULL, account_id INTEGER NOT NULL, target_account_id INTEGER, category_id INTEGER, provision_id INTEGER, date TEXT NOT NULL, note TEXT, is_pending INTEGER NOT NULL DEFAULT 0, local_operation_id INTEGER, updated_at TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE dashboard_cache ( id INTEGER PRIMARY KEY CHECK (id = 1), json TEXT NOT NULL, updated_at TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE pending_operations ( id INTEGER PRIMARY KEY AUTOINCREMENT, operation_type TEXT NOT NULL, payload_json TEXT NOT NULL, created_at TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', error TEXT ) '''); } Future getSetting(String key) async { final db = await database; final rows = await db.query( 'settings', where: 'key = ?', whereArgs: [key], limit: 1, ); if (rows.isEmpty) return null; return rows.first['value'] as String?; } Future setSetting(String key, String value) async { final db = await database; await db.insert( 'settings', {'key': key, 'value': value}, conflictAlgorithm: ConflictAlgorithm.replace, ); } Future getSyncState(String key) async { final db = await database; final rows = await db.query( 'sync_state', where: 'key = ?', whereArgs: [key], limit: 1, ); if (rows.isEmpty) return null; return rows.first['value'] as String?; } Future setSyncState(String key, String value) async { final db = await database; await db.insert( 'sync_state', {'key': key, 'value': value}, conflictAlgorithm: ConflictAlgorithm.replace, ); } Future getLastSyncAt() async { final value = await getSyncState('last_successful_sync_at'); return value == null ? null : DateTime.tryParse(value); } Future saveServerSnapshot({ required DashboardData dashboard, required List accounts, required List categories, required List transactions, }) async { final db = await database; final now = DateTime.now().toIso8601String(); await db.transaction((txn) async { await txn.delete('accounts_cache'); await txn.delete('categories_cache'); await txn.delete('transactions_cache'); await txn.delete('dashboard_cache'); for (final account in accounts) { await txn.insert( 'accounts_cache', account.toDbMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } for (final category in categories) { await txn.insert( 'categories_cache', category.toDbMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } for (final transaction in transactions) { await txn.insert( 'transactions_cache', transaction.toDbMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } await txn.insert( 'dashboard_cache', { 'id': 1, 'json': jsonEncode(dashboard.toJson()), 'updated_at': now, }, conflictAlgorithm: ConflictAlgorithm.replace, ); await txn.insert( 'sync_state', { 'key': 'last_successful_sync_at', 'value': now, }, conflictAlgorithm: ConflictAlgorithm.replace, ); }); } Future> loadAccounts() async { final db = await database; final rows = await db.query('accounts_cache', orderBy: 'name COLLATE NOCASE ASC'); return rows.map(Account.fromDbMap).toList(); } Future> loadCategories() async { final db = await database; final rows = await db.query('categories_cache', orderBy: 'type ASC, name COLLATE NOCASE ASC'); return rows.map(Category.fromDbMap).toList(); } Future> loadTransactions({int limit = 50}) async { final db = await database; final rows = await db.query( 'transactions_cache', orderBy: 'date DESC, id DESC', limit: limit, ); return rows.map(TransactionItem.fromDbMap).toList(); } Future loadDashboard() async { final db = await database; final rows = await db.query( 'dashboard_cache', where: 'id = 1', limit: 1, ); if (rows.isEmpty) return null; final decoded = jsonDecode(rows.first['json'] as String) as Map; return DashboardData.fromJson(decoded); } Future enqueueCreateTransaction(Map payload) async { final db = await database; return db.insert( 'pending_operations', { 'operation_type': 'create_transaction', 'payload_json': jsonEncode(payload), 'created_at': DateTime.now().toIso8601String(), 'status': 'pending', 'error': null, }, ); } Future> loadPendingOperations() async { final db = await database; final rows = await db.query( 'pending_operations', where: 'status = ?', whereArgs: ['pending'], orderBy: 'created_at ASC, id ASC', ); return rows.map(PendingOperation.fromDbMap).toList(); } Future pendingCount() async { final db = await database; final result = await db.rawQuery( "SELECT COUNT(*) AS count FROM pending_operations WHERE status = 'pending'", ); return Sqflite.firstIntValue(result) ?? 0; } Future deletePendingOperation(int id) async { final db = await database; await db.delete( 'pending_operations', where: 'id = ?', whereArgs: [id], ); } Future markPendingError(int id, String error) async { final db = await database; await db.update( 'pending_operations', { 'error': error, 'status': 'pending', }, where: 'id = ?', whereArgs: [id], ); } }