Initial upload: fin65-mobile
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
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<Database> 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<void> _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<String?> 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<void> setSetting(String key, String value) async {
|
||||
final db = await database;
|
||||
await db.insert(
|
||||
'settings',
|
||||
{'key': key, 'value': value},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> 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<void> setSyncState(String key, String value) async {
|
||||
final db = await database;
|
||||
await db.insert(
|
||||
'sync_state',
|
||||
{'key': key, 'value': value},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<DateTime?> getLastSyncAt() async {
|
||||
final value = await getSyncState('last_successful_sync_at');
|
||||
return value == null ? null : DateTime.tryParse(value);
|
||||
}
|
||||
|
||||
Future<void> saveServerSnapshot({
|
||||
required DashboardData dashboard,
|
||||
required List<Account> accounts,
|
||||
required List<Category> categories,
|
||||
required List<TransactionItem> 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<List<Account>> 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<List<Category>> 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<List<TransactionItem>> 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<DashboardData?> 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<String, dynamic>;
|
||||
return DashboardData.fromJson(decoded);
|
||||
}
|
||||
|
||||
Future<int> enqueueCreateTransaction(Map<String, dynamic> 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<List<PendingOperation>> 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<int> 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<void> deletePendingOperation(int id) async {
|
||||
final db = await database;
|
||||
await db.delete(
|
||||
'pending_operations',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> markPendingError(int id, String error) async {
|
||||
final db = await database;
|
||||
await db.update(
|
||||
'pending_operations',
|
||||
{
|
||||
'error': error,
|
||||
'status': 'pending',
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user