This commit is contained in:
2026-03-23 12:17:04 +03:00
parent 598254556c
commit 8f03af4b0a
6 changed files with 164 additions and 38 deletions
+34 -1
View File
@@ -13,12 +13,15 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 4;
int get schemaVersion => 5;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
print('--- DATABASE MIGRATION: from=$from to=$to ---');
if (from == 1) {
print('Migration: Creating accounts table');
await migrator.createTable(accounts);
await customStatement(
'INSERT INTO accounts (name, is_main, currency, sort_order, created_at) '
@@ -26,11 +29,41 @@ class AppDatabase extends _$AppDatabase {
['main', 1, 'USD', 0, DateTime.now().millisecondsSinceEpoch],
);
}
if (from == 2) {
print('Migration: Adding currency column to accounts');
await customStatement(
'ALTER TABLE accounts ADD COLUMN currency TEXT NOT NULL DEFAULT "USD"',
);
}
// Add account_id column to transactions if upgrading from version < 5
if (from < 5) {
print('Migration: Adding account_id column to transactions');
try {
// Check if column exists first
final result = await customSelect(
'PRAGMA table_info(transactions)',
).get();
final hasAccountId = result.any((row) => row.data['name'] == 'account_id');
if (!hasAccountId) {
print('Migration: account_id column does not exist, adding it now');
await customStatement(
'ALTER TABLE transactions ADD COLUMN account_id INTEGER NOT NULL DEFAULT 1',
);
print('Migration: account_id column added successfully');
} else {
print('Migration: account_id column already exists, skipping');
}
} catch (e) {
print('Migration: Error adding account_id column: $e');
// If the column already exists, this will fail, which is fine
}
}
print('--- DATABASE MIGRATION: COMPLETE ---');
},
);
+6 -8
View File
@@ -122,7 +122,8 @@ class $TransactionsTable extends Transactions
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
requiredDuringInsert: false,
defaultValue: const Constant(1),
);
static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt',
@@ -241,8 +242,6 @@ class $TransactionsTable extends Transactions
_accountIdMeta,
accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta),
);
} else if (isInserting) {
context.missing(_accountIdMeta);
}
if (data.containsKey('created_at')) {
context.handle(
@@ -567,15 +566,14 @@ class TransactionsCompanion extends UpdateCompanion<Transaction> {
this.lastOccurrence = const Value.absent(),
this.currency = const Value.absent(),
this.currencyCode = const Value.absent(),
required int accountId,
this.accountId = const Value.absent(),
this.createdAt = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
amount = Value(amount),
category = Value(category),
type = Value(type),
date = Value(date),
accountId = Value(accountId);
date = Value(date);
static Insertable<Transaction> custom({
Expression<String>? id,
Expression<double>? amount,
@@ -2324,7 +2322,7 @@ typedef $$TransactionsTableCreateCompanionBuilder =
Value<DateTime?> lastOccurrence,
Value<String> currency,
Value<String> currencyCode,
required int accountId,
Value<int> accountId,
Value<DateTime> createdAt,
Value<int> rowid,
});
@@ -2608,7 +2606,7 @@ class $$TransactionsTableTableManager
Value<DateTime?> lastOccurrence = const Value.absent(),
Value<String> currency = const Value.absent(),
Value<String> currencyCode = const Value.absent(),
required int accountId,
Value<int> accountId = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => TransactionsCompanion.insert(
+1 -1
View File
@@ -12,7 +12,7 @@ class Transactions extends Table {
DateTimeColumn get lastOccurrence => dateTime().nullable()();
TextColumn get currency => text().withDefault(const Constant('\$'))();
TextColumn get currencyCode => text().withDefault(const Constant('USD'))();
IntColumn get accountId => integer()();
IntColumn get accountId => integer().withDefault(const Constant(1))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
+55 -9
View File
@@ -24,7 +24,7 @@ class AccountRepository {
// Fallback: insert default account if none exists
await _db.into(_db.accounts).insert(
AccountsCompanion.insert(
name: 'Main',
name: 'main',
isMain: const Value(true),
currency: const Value('USD'),
sortOrder: const Value(0),
@@ -69,7 +69,7 @@ class AccountRepository {
try {
await _db.into(_db.accounts).insert(
AccountsCompanion.insert(
name: 'Main',
name: 'main',
isMain: const Value(true),
currency: const Value('USD'),
sortOrder: const Value(0),
@@ -103,15 +103,61 @@ class AccountRepository {
Future<model.Account> getMain() async {
final row = await (_db.select(_db.accounts)
..where((a) => a.isMain.equals(true)))
.getSingle();
.getSingleOrNull();
if (row != null) {
return model.Account(
id: row.id,
name: row.name,
isMain: row.isMain,
sortOrder: row.sortOrder,
currency: row.currency,
createdAt: row.createdAt,
);
}
// Fallback if no main account is found
final all = await getAll();
if (all.isNotEmpty) return all.first;
// Absolute fallback: create and return a default account
try {
await _db.into(_db.accounts).insert(
AccountsCompanion.insert(
name: 'main',
isMain: const Value(true),
currency: const Value('USD'),
sortOrder: const Value(0),
),
);
// Query the newly created account
final newRow = await (_db.select(_db.accounts)
..where((a) => a.isMain.equals(true)))
.getSingleOrNull();
if (newRow != null) {
return model.Account(
id: newRow.id,
name: newRow.name,
isMain: newRow.isMain,
sortOrder: newRow.sortOrder,
currency: newRow.currency,
createdAt: newRow.createdAt,
);
}
} catch (_) {
// Ignore insert errors
}
// Final fallback to prevent crashes
return model.Account(
id: row.id,
name: row.name,
isMain: row.isMain,
sortOrder: row.sortOrder,
currency: row.currency,
createdAt: row.createdAt,
id: 1,
name: 'main',
isMain: true,
sortOrder: 0,
currency: 'USD',
createdAt: DateTime.now(),
);
}
}
@@ -56,12 +56,24 @@ class TransactionRepository {
/// Add transaction
Future<Result<void>> add(model.Transaction transaction) async {
return asyncResultOf(() async {
print('--- SAVING TRANSACTION: START ---');
print('Transaction data: ID=${transaction.id}, Amount=${transaction.amount}, AccId=${transaction.accountId}');
print('Category=${transaction.category}, Type=${transaction.type.name}');
print('Date=${transaction.date}, Currency=${transaction.currencyCode}');
final companion = _toCompanion(transaction);
print('Companion created successfully');
print('Companion: $companion');
final result = await _db.insertTransaction(companion);
print('DB Insert finished. Result Success: ${result.isSuccess}');
if (result.isFailure) {
print('!!! DB INSERT FAILED: ${result.errorOrNull}');
throw Exception(result.errorOrNull);
}
print('--- SAVING TRANSACTION: END (SUCCESS) ---');
});
}
@@ -173,19 +185,29 @@ class TransactionRepository {
/// Convert app model to companion for insert
TransactionsCompanion _toCompanion(model.Transaction transaction) {
return TransactionsCompanion(
id: Value(transaction.id),
amount: Value(transaction.amount),
category: Value(transaction.category),
type: Value(transaction.type.name),
date: Value(transaction.date),
note: Value(transaction.note),
recurrence: Value(transaction.recurrence.name),
lastOccurrence: Value(transaction.lastOccurrence),
currency: Value(transaction.currency),
currencyCode: Value(transaction.currencyCode),
accountId: Value(transaction.accountId),
);
try {
print('_toCompanion: Creating companion for transaction ${transaction.id}');
final companion = TransactionsCompanion(
id: Value(transaction.id),
amount: Value(transaction.amount),
category: Value(transaction.category),
type: Value(transaction.type.name),
date: Value(transaction.date),
note: Value(transaction.note),
recurrence: Value(transaction.recurrence.name),
lastOccurrence: Value(transaction.lastOccurrence),
currency: Value(transaction.currency),
currencyCode: Value(transaction.currencyCode),
accountId: Value(transaction.accountId),
);
print('_toCompanion: Companion created successfully');
return companion;
} catch (e, stack) {
print('!!! _toCompanion FAILED !!!');
print('Error: $e');
print('Stack: $stack');
rethrow;
}
}
/// Parse recurrence type
+33 -6
View File
@@ -154,14 +154,19 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
: _noteController.text.trim();
try {
// Get account ID: use active account or fallback to main
print('--- SUBMIT CLICKED ---');
print('Amount: $amount, Category: ${state.category}, Type: ${state.type.name}');
final activeAccount = ref.read(activeAccountProvider);
int accountId;
if (activeAccount != null) {
print('Using active account ID: ${activeAccount.id}, Name: ${activeAccount.name}');
accountId = activeAccount.id;
} else {
print('No active account. Fetching main account...');
final mainAccount = await ref.read(accountRepositoryProvider).getMain();
print('Main account fetched: ID=${mainAccount.id}, Name: ${mainAccount.name}');
accountId = mainAccount.id;
}
@@ -176,21 +181,43 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
currencyCode: state.overrideCurrencyCode,
accountId: accountId,
);
print('Transaction object created: ID=${tx.id}, AccId=${tx.accountId}');
print('Calling provider to save...');
if (state.isEditing) {
await ref.read(transactionsProvider.notifier).update(tx);
print('Update completed');
} else {
await ref.read(transactionsProvider.notifier).add(tx);
final res = await ref.read(transactionsProvider.notifier).add(tx);
print('Add completed. Result: ${res.isSuccess ? "SUCCESS" : "FAILURE"}');
if (res.isFailure) {
print('!!! Provider returned failure: ${res.errorOrNull}');
throw Exception(res.errorOrNull);
}
}
print('Provider save completed successfully');
HapticService.medium();
if (mounted) context.pop();
} catch (e) {
// Handle error silently or show a snackbar
if (mounted) {
print('Popping screen...');
context.pop();
}
} catch (e, stack) {
print('!!! SAVE CRASHED !!!');
print('Error: $e');
print('Stack trace:');
print(stack);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error saving transaction: $e')),
SnackBar(
content: Text('Save error: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
),
);
}
} finally {