diff --git a/lib/app/theme.dart b/lib/app/theme.dart index 6c39ca7..d8ecb58 100644 --- a/lib/app/theme.dart +++ b/lib/app/theme.dart @@ -135,7 +135,7 @@ class AppTheme { final textTheme = GoogleFonts.poppinsTextTheme(base.textTheme).apply( bodyColor: const Color(0xFF1A1A2E), displayColor: const Color(0xFF1A1A2E), - fontFamilyFallback: ['Roboto'], // Ensures Cyrillic renders with same visual style + fontFamilyFallback: ['Roboto'], ); return base.copyWith( diff --git a/lib/core/constants.dart b/lib/core/constants.dart index e34d577..6147868 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -140,7 +140,6 @@ class AppCurrencies { } } -/// `(code, textSymbol)` for pickers and conversion rows. BYN uses `''` — use BynSign in UI. const List<(String, String)> kDisplayCurrencies = [ ('USD', r'$'), ('EUR', '€'), diff --git a/lib/core/services/haptic_service.dart b/lib/core/services/haptic_service.dart index e8795c2..6af8d95 100644 --- a/lib/core/services/haptic_service.dart +++ b/lib/core/services/haptic_service.dart @@ -3,7 +3,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class HapticService { static const _key = 'haptic_enabled'; - static bool _enabled = true; // runtime cache + static bool _enabled = true; static Future init() async { final prefs = await SharedPreferences.getInstance(); diff --git a/lib/core/utils/result.dart b/lib/core/utils/result.dart index 1c55f33..539696d 100644 --- a/lib/core/utils/result.dart +++ b/lib/core/utils/result.dart @@ -1,27 +1,20 @@ -/// Result type for handling success and failure cases -/// Uses sealed classes for exhaustive pattern matching sealed class Result { const Result(); - /// Check if result is success bool get isSuccess => this is Success; - /// Check if result is failure bool get isFailure => this is Failure; - /// Get data if success, null otherwise T? get dataOrNull => switch (this) { Success(data: final data) => data, Failure() => null, }; - /// Get error if failure, null otherwise String? get errorOrNull => switch (this) { Success() => null, Failure(message: final message) => message, }; - /// Transform success value Result map(R Function(T data) transform) { return switch (this) { Success(data: final data) => Success(transform(data)), @@ -29,7 +22,6 @@ sealed class Result { }; } - /// Execute callback on success Result onSuccess(void Function(T data) callback) { if (this case Success(data: final data)) { callback(data); @@ -37,7 +29,6 @@ sealed class Result { return this; } - /// Execute callback on failure Result onFailure(void Function(String message) callback) { if (this case Failure(message: final message)) { callback(message); @@ -45,7 +36,6 @@ sealed class Result { return this; } - /// Get data or throw exception T getOrThrow() { return switch (this) { Success(data: final data) => data, @@ -54,7 +44,6 @@ sealed class Result { }; } - /// Get data or return default value T getOrDefault(T defaultValue) { return switch (this) { Success(data: final data) => data, @@ -62,7 +51,6 @@ sealed class Result { }; } - /// Get data or compute from error T getOrElse(T Function(String error) onError) { return switch (this) { Success(data: final data) => data, @@ -71,7 +59,6 @@ sealed class Result { } } -/// Success result containing data class Success extends Result { final T data; @@ -91,7 +78,6 @@ class Success extends Result { int get hashCode => data.hashCode; } -/// Failure result containing error message and optional exception class Failure extends Result { final String message; final Exception? exception; @@ -114,15 +100,12 @@ class Failure extends Result { int get hashCode => message.hashCode ^ exception.hashCode; } -/// Extension for Future> extension FutureResultExtension on Future> { - /// Map async result Future> mapAsync(R Function(T data) transform) async { final result = await this; return result.map(transform); } - /// Execute callback on success Future> onSuccessAsync( Future Function(T data) callback, ) async { @@ -133,7 +116,6 @@ extension FutureResultExtension on Future> { return result; } - /// Execute callback on failure Future> onFailureAsync( Future Function(String message) callback, ) async { @@ -145,7 +127,6 @@ extension FutureResultExtension on Future> { } } -/// Helper to wrap try-catch blocks Result resultOf(T Function() computation) { try { return Success(computation()); @@ -154,7 +135,6 @@ Result resultOf(T Function() computation) { } } -/// Helper to wrap async try-catch blocks Future> asyncResultOf(Future Function() computation) async { try { final data = await computation(); diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index d5b9299..72b7b36 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -18,10 +18,7 @@ class AppDatabase extends _$AppDatabase { @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) ' @@ -31,17 +28,13 @@ class AppDatabase extends _$AppDatabase { } 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(); @@ -49,36 +42,23 @@ class AppDatabase extends _$AppDatabase { 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 ---'); }, ); - // ============================================================================ - // TRANSACTIONS - // ============================================================================ - - /// Get all transactions ordered by date descending Future> getAllTransactions() { return (select( transactions, )..orderBy([(t) => OrderingTerm.desc(t.date)])).get(); } - /// Get transactions by date range Future> getTransactionsByDateRange( DateTime start, DateTime end, @@ -90,7 +70,6 @@ class AppDatabase extends _$AppDatabase { .get(); } - /// Get transactions by type Future> getTransactionsByType(String type) { return (select(transactions) ..where((t) => t.type.equals(type)) @@ -98,7 +77,6 @@ class AppDatabase extends _$AppDatabase { .get(); } - /// Get transactions by category Future> getTransactionsByCategory(String category) { return (select(transactions) ..where((t) => t.category.equals(category)) @@ -106,7 +84,6 @@ class AppDatabase extends _$AppDatabase { .get(); } - /// Search transactions by note or category Future> searchTransactions(String query) { final lowerQuery = query.toLowerCase(); return (select(transactions) @@ -119,14 +96,12 @@ class AppDatabase extends _$AppDatabase { .get(); } - /// Get transaction by ID Future getTransactionById(String id) { return (select( transactions, )..where((t) => t.id.equals(id))).getSingleOrNull(); } - /// Insert transaction Future> insertTransaction( TransactionsCompanion transaction, ) async { @@ -135,7 +110,6 @@ class AppDatabase extends _$AppDatabase { }); } - /// Update transaction Future> updateTransaction(dynamic transaction) async { return asyncResultOf(() async { final companion = transaction as TransactionsCompanion; @@ -149,7 +123,6 @@ class AppDatabase extends _$AppDatabase { }); } - /// Delete transaction Future> deleteTransaction(String id) async { return asyncResultOf(() async { final deleted = await (delete( @@ -162,90 +135,64 @@ class AppDatabase extends _$AppDatabase { }); } - /// Delete all transactions Future deleteAllTransactions() { return delete(transactions).go(); } - /// Get recurring transactions that need processing Future> getRecurringTransactions() { return (select( transactions, )..where((t) => t.recurrence.equals('none').not())).get(); } - // ============================================================================ - // CATEGORIES - // ============================================================================ - - /// Get all categories Future> getAllCategories() { return select(categories).get(); } - /// Get categories by type Future> getCategoriesByType(String type) { return (select(categories)..where((c) => c.type.equals(type))).get(); } - /// Insert category Future insertCategory(CategoriesCompanion category) { return into(categories).insert(category); } - /// Update category Future updateCategory(Category category) { return update(categories).replace(category); } - /// Delete category Future deleteCategory(int id) { return (delete(categories)..where((c) => c.id.equals(id))).go(); } - // ============================================================================ - // BUDGETS - // ============================================================================ - - /// Get budget for month/year Future getBudget(int month, int year) { return (select(budgets) ..where((b) => b.month.equals(month) & b.year.equals(year))) .getSingleOrNull(); } - /// Insert or update budget Future upsertBudget(BudgetsCompanion budget) { return into(budgets).insertOnConflictUpdate(budget); } - /// Delete budget Future deleteBudget(int id) { return (delete(budgets)..where((b) => b.id.equals(id))).go(); } - // ============================================================================ - // EXCHANGE RATES - // ============================================================================ - - /// Get exchange rate Future getExchangeRate(String from, String to) { return (select(exchangeRates) ..where((r) => r.fromCurrency.equals(from) & r.toCurrency.equals(to))) .getSingleOrNull(); } - /// Insert or update exchange rate Future upsertExchangeRate(ExchangeRatesCompanion rate) { return into(exchangeRates).insertOnConflictUpdate(rate); } - /// Get all exchange rates Future> getAllExchangeRates() { return select(exchangeRates).get(); } - /// Delete old exchange rates (older than 24 hours) Future deleteOldExchangeRates() { final yesterday = DateTime.now().subtract(const Duration(hours: 24)); return (delete( @@ -253,11 +200,6 @@ class AppDatabase extends _$AppDatabase { )..where((r) => r.updatedAt.isSmallerThanValue(yesterday))).go(); } - // ============================================================================ - // STATISTICS & AGGREGATIONS - // ============================================================================ - - /// Get total balance Future getTotalBalance() async { final txs = await getAllTransactions(); return txs.fold(0.0, (sum, tx) { @@ -265,7 +207,6 @@ class AppDatabase extends _$AppDatabase { }); } - /// Get total income for date range Future getTotalIncome(DateTime start, DateTime end) async { final txs = await getTransactionsByDateRange(start, end); return txs @@ -273,7 +214,6 @@ class AppDatabase extends _$AppDatabase { .fold(0.0, (sum, tx) => sum + tx.amount); } - /// Get total expense for date range Future getTotalExpense(DateTime start, DateTime end) async { final txs = await getTransactionsByDateRange(start, end); return txs @@ -281,7 +221,6 @@ class AppDatabase extends _$AppDatabase { .fold(0.0, (sum, tx) => sum + tx.amount); } - /// Get category totals for date range Future> getCategoryTotals( DateTime start, DateTime end, @@ -298,11 +237,6 @@ class AppDatabase extends _$AppDatabase { return totals; } - // ============================================================================ - // ACCOUNTS - // ============================================================================ - - /// Update an account Future updateAccount(AccountsCompanion account) async { await update(accounts).replace(account); } diff --git a/lib/data/database/tables.dart b/lib/data/database/tables.dart index 0e5c151..f90e1d5 100644 --- a/lib/data/database/tables.dart +++ b/lib/data/database/tables.dart @@ -1,11 +1,10 @@ import 'package:drift/drift.dart'; -/// Transactions table class Transactions extends Table { TextColumn get id => text()(); RealColumn get amount => real()(); TextColumn get category => text()(); - TextColumn get type => text()(); // 'income' or 'expense' + TextColumn get type => text()(); DateTimeColumn get date => dateTime()(); TextColumn get note => text().nullable()(); TextColumn get recurrence => text().withDefault(const Constant('none'))(); @@ -19,18 +18,16 @@ class Transactions extends Table { Set get primaryKey => {id}; } -/// Categories table for custom categories class Categories extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text().withLength(min: 1, max: 50)(); - TextColumn get type => text()(); // 'income' or 'expense' + TextColumn get type => text()(); TextColumn get icon => text().nullable()(); TextColumn get color => text().nullable()(); BoolColumn get isDefault => boolean().withDefault(const Constant(false))(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } -/// Budgets table for monthly budgets class Budgets extends Table { IntColumn get id => integer().autoIncrement()(); RealColumn get amount => real()(); @@ -40,7 +37,6 @@ class Budgets extends Table { DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); } -/// Exchange rates cache class ExchangeRates extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get fromCurrency => text()(); @@ -49,7 +45,6 @@ class ExchangeRates extends Table { DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); } -/// Accounts table for multi-account support class Accounts extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); diff --git a/lib/data/repositories/account_repository.dart b/lib/data/repositories/account_repository.dart index 3433419..2d04c6a 100644 --- a/lib/data/repositories/account_repository.dart +++ b/lib/data/repositories/account_repository.dart @@ -21,7 +21,6 @@ class AccountRepository { .watch() .asyncMap((rows) async { if (rows.isEmpty) { - // Fallback: insert default account if none exists await _db.into(_db.accounts).insert( AccountsCompanion.insert( name: 'main', @@ -30,7 +29,6 @@ class AccountRepository { sortOrder: const Value(0), ), ); - // Re-query after insert final newRows = await (_db.select(_db.accounts) ..orderBy([(a) => OrderingTerm.asc(a.sortOrder)])) .get(); @@ -63,8 +61,6 @@ class AccountRepository { var rows = await (_db.select(_db.accounts) ..orderBy([(a) => OrderingTerm.asc(a.sortOrder)])) .get(); - - // Fallback: insert default account if none exists if (rows.isEmpty) { try { await _db.into(_db.accounts).insert( @@ -78,7 +74,6 @@ class AccountRepository { } catch (e) { // Ignore if already exists } - // Re-query after insert rows = await (_db.select(_db.accounts) ..orderBy([(a) => OrderingTerm.asc(a.sortOrder)])) .get(); @@ -95,7 +90,6 @@ class AccountRepository { )) .toList(); } catch (e) { - // Return empty list on error return []; } } @@ -116,11 +110,9 @@ class AccountRepository { ); } - // 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( @@ -131,7 +123,6 @@ class AccountRepository { ), ); - // Query the newly created account final newRow = await (_db.select(_db.accounts) ..where((a) => a.isMain.equals(true))) .getSingleOrNull(); @@ -150,7 +141,6 @@ class AccountRepository { // Ignore insert errors } - // Final fallback to prevent crashes return model.Account( id: 1, name: 'main', diff --git a/lib/data/repositories/transaction_repository.dart b/lib/data/repositories/transaction_repository.dart index 0bd6b5e..5cc473f 100644 --- a/lib/data/repositories/transaction_repository.dart +++ b/lib/data/repositories/transaction_repository.dart @@ -8,7 +8,6 @@ class TransactionRepository { TransactionRepository(this._db); - /// Get all transactions Future>> getAll() async { return asyncResultOf(() async { final transactions = await _db.getAllTransactions(); @@ -16,7 +15,6 @@ class TransactionRepository { }); } - /// Get transactions by date range Future>> getByDateRange( DateTime start, DateTime end, @@ -27,7 +25,6 @@ class TransactionRepository { }); } - /// Get transactions by type Future>> getByType( model.TransactionType type, ) async { @@ -37,7 +34,6 @@ class TransactionRepository { }); } - /// Search transactions Future>> search(String query) async { return asyncResultOf(() async { final transactions = await _db.searchTransactions(query); @@ -45,7 +41,6 @@ class TransactionRepository { }); } - /// Get transaction by ID Future> getById(String id) async { return asyncResultOf(() async { final transaction = await _db.getTransactionById(id); @@ -53,31 +48,17 @@ class TransactionRepository { }); } - /// Add transaction Future> 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) ---'); }); } - /// Update transaction Future> update(model.Transaction transaction) async { return asyncResultOf(() async { final dbTransaction = _toDbModel(transaction); @@ -89,19 +70,16 @@ class TransactionRepository { }); } - /// Delete transaction Future> delete(String id) async { return _db.deleteTransaction(id); } - /// Delete all transactions Future> deleteAll() async { return asyncResultOf(() async { await _db.deleteAllTransactions(); }); } - /// Get recurring transactions Future>> getRecurring() async { return asyncResultOf(() async { final transactions = await _db.getRecurringTransactions(); @@ -109,28 +87,24 @@ class TransactionRepository { }); } - /// Get total balance Future> getTotalBalance() async { return asyncResultOf(() async { return await _db.getTotalBalance(); }); } - /// Get total income for date range Future> getTotalIncome(DateTime start, DateTime end) async { return asyncResultOf(() async { return await _db.getTotalIncome(start, end); }); } - /// Get total expense for date range Future> getTotalExpense(DateTime start, DateTime end) async { return asyncResultOf(() async { return await _db.getTotalExpense(start, end); }); } - /// Get category totals Future>> getCategoryTotals( DateTime start, DateTime end, @@ -141,11 +115,6 @@ class TransactionRepository { }); } - // ============================================================================ - // CONVERTERS - // ============================================================================ - - /// Convert database model to app model model.Transaction _toModel(dynamic dbTransaction) { return model.Transaction( id: dbTransaction.id as String, @@ -164,9 +133,7 @@ class TransactionRepository { ); } - /// Convert app model to database model dynamic _toDbModel(model.Transaction transaction) { - // This will be replaced with proper TransactionData after code generation return TransactionsCompanion( id: Value(transaction.id), amount: Value(transaction.amount), @@ -183,10 +150,8 @@ class TransactionRepository { ); } - /// Convert app model to companion for insert TransactionsCompanion _toCompanion(model.Transaction transaction) { try { - print('_toCompanion: Creating companion for transaction ${transaction.id}'); final companion = TransactionsCompanion( id: Value(transaction.id), amount: Value(transaction.amount), @@ -200,17 +165,12 @@ class TransactionRepository { 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'); + } catch (e, _) { rethrow; } } - /// Parse recurrence type model.RecurrenceType _parseRecurrence(String recurrence) { return model.RecurrenceType.values.firstWhere( (e) => e.name == recurrence, diff --git a/lib/features/add_transaction/provider.dart b/lib/features/add_transaction/provider.dart index c54b4a5..78cae11 100644 --- a/lib/features/add_transaction/provider.dart +++ b/lib/features/add_transaction/provider.dart @@ -30,7 +30,6 @@ class AddTransactionState { }); factory AddTransactionState.fromTransaction(Transaction tx) { - // Override type to transfer when category is 'Transfer' final resolvedType = (tx.category == 'Transfer') ? TransactionType.transfer : tx.type; diff --git a/lib/features/add_transaction/screen.dart b/lib/features/add_transaction/screen.dart index 0dc19a9..4535eee 100644 --- a/lib/features/add_transaction/screen.dart +++ b/lib/features/add_transaction/screen.dart @@ -206,7 +206,6 @@ class _AddTransactionScreenState extends ConsumerState final amount = double.parse(parsed); final state = ref.read(addTransactionProvider(widget.initial)); - // Validate transfer if (state.type == TransactionType.transfer) { bool hasError = false; @@ -471,14 +470,11 @@ class _AddTransactionScreenState extends ConsumerState onPressed: () async { Navigator.pop(ctx); - // Always delete the record we were given await ref .read(transactionsProvider.notifier) .delete(widget.initial!.id); - // If this is a Transfer, also delete the counterpart if (widget.initial!.category == 'Transfer') { - // Use the pre-populated IDs from initState if available final counterpartId = widget.initial!.type == TransactionType.expense ? _transferIncomeRecordId @@ -489,7 +485,6 @@ class _AddTransactionScreenState extends ConsumerState .read(transactionsProvider.notifier) .delete(counterpartId); } else { - // Fallback: search manually final allTxs = ref.read(transactionsProvider).valueOrNull ?? []; @@ -893,10 +888,9 @@ class _ToAccountDropdownOverlay extends ConsumerWidget { .selectedAccountId; final toAccountId = ref.read(addTransactionProvider(initial)).toAccountId; - // Calculate position from trigger key double top = 340; double left = 20; - double triggerWidth = 200; // fallback width + double triggerWidth = 200; if (triggerKey?.currentContext != null) { final triggerBox = diff --git a/lib/features/add_transaction/widgets/account_selector.dart b/lib/features/add_transaction/widgets/account_selector.dart index 6056be1..6824457 100644 --- a/lib/features/add_transaction/widgets/account_selector.dart +++ b/lib/features/add_transaction/widgets/account_selector.dart @@ -134,10 +134,9 @@ class AccountDropdownOverlay extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final accountsAsync = ref.watch(accountsProvider); - // Calculate position from trigger key double top = 76; double left = 20; - double triggerWidth = 200; // fallback width + double triggerWidth = 200; if (triggerKey?.currentContext != null) { final triggerBox = diff --git a/lib/features/add_transaction/widgets/submit_button.dart b/lib/features/add_transaction/widgets/submit_button.dart index eff5be7..93c8e6a 100644 --- a/lib/features/add_transaction/widgets/submit_button.dart +++ b/lib/features/add_transaction/widgets/submit_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import '../../../core/constants.dart'; import '../../../shared/models/transaction.dart'; class SubmitButton extends StatelessWidget { diff --git a/lib/features/dashboard/provider.dart b/lib/features/dashboard/provider.dart index 5fc13fe..c975ed5 100644 --- a/lib/features/dashboard/provider.dart +++ b/lib/features/dashboard/provider.dart @@ -12,15 +12,6 @@ import '../../shared/models/account.dart'; import '../../shared/services/storage_service.dart'; import '../settings/provider.dart'; -// BUG FOUND: lib/features/dashboard/provider.dart -// Description: CardColorsNotifier calls an async `_load()` in the constructor without awaiting it. -// If the user triggers `save()` before `_load()` completes, the late `_load()` can -// overwrite the newly saved colors/gradient types. -// Reproduction: Open the app (cold start), open the card color editor immediately, press Apply -// before the initial load finishes. -// Suggested fix: Track a generation/token for in-flight loads and ignore stale load results -// after any state mutation (save/reset/theme-change). - final sharedPreferencesProvider = Provider((ref) { throw UnimplementedError('Override in main'); }); @@ -113,15 +104,13 @@ class TransactionsNotifier } } -// Returns a map: transactionId -> paired Transaction (its counterpart) final transferPairsProvider = Provider>((ref) { final txs = ref.watch(transactionsProvider).valueOrNull ?? []; final transfers = txs.where((t) => t.category == 'Transfer').toList(); final Map pairs = {}; for (final tx in transfers) { - if (pairs.containsKey(tx.id)) continue; // already paired - // Find counterpart: opposite type, same amount, same date (minute precision), same note + if (pairs.containsKey(tx.id)) continue; final counterpart = transfers.firstWhereOrNull( (other) => other.id != tx.id && @@ -156,18 +145,15 @@ final timeFilterProvider = StateProvider( (ref) => TimeFilter.lastMonth, ); -// Base filtered transactions by active account final accountFilteredTransactionsProvider = Provider>((ref) { final txsAsync = ref.watch(transactionsProvider); final txs = txsAsync.valueOrNull ?? []; final activeAccount = ref.watch(activeAccountProvider); - // If activeAccount is null (Total Balance page), return all transactions if (activeAccount == null) { return txs; } - // Filter by account ID return txs.where((t) => t.accountId == activeAccount.id).toList(); }); @@ -214,18 +200,15 @@ final totalBalanceProvider = Provider((ref) { }); final totalIncomeProvider = Provider((ref) { - // Watch the filtered transactions directly final txs = ref.watch(accountFilteredTransactionsProvider); final filtered = txs.where( (t) => t.type == TransactionType.income && t.category != 'Transfer', ); - // Watch the dependencies that change on swipe! final index = ref.watch(activeAccountIndexProvider); final accountsAsync = ref.watch(accountsProvider); final globalCurrency = ref.watch(currencyProvider).code; - // Resolve target currency synchronously based on the current swipe index String targetCurrency = globalCurrency; if (index > 0) { final accounts = accountsAsync.valueOrNull ?? []; @@ -341,12 +324,9 @@ final filteredTransactionsProvider = Provider>((ref) { filtered.sort((a, b) => b.date.compareTo(a.date)); - // Deduplicate transfers for Total Balance view if (activeAccount == null) { filtered = filtered.where((t) { if (t.category != 'Transfer') return true; - // On Total Balance: show only expense side of complete pairs - // If income side has a known pair, hide it if (t.type == TransactionType.income && transferPairs.containsKey(t.id)) { return false; } @@ -361,7 +341,6 @@ final recentTransactionsProvider = Provider>((ref) { return ref.watch(filteredTransactionsProvider).take(20).toList(); }); -// Watches the list of all accounts final accountsProvider = StreamProvider>((ref) async* { final repository = ref.watch(accountRepositoryProvider); while (true) { @@ -370,15 +349,13 @@ final accountsProvider = StreamProvider>((ref) async* { } }); -// Ephemeral UI state — active carousel index, starts at 0, not persisted final activeAccountIndexProvider = StateProvider((ref) => 0); -// Returns the currently active Account based on carousel index final activeAccountProvider = Provider((ref) { final index = ref.watch(activeAccountIndexProvider); final accountsAsync = ref.watch(accountsProvider); - if (index == 0) return null; // 0 means "Total Balance" + if (index == 0) return null; return accountsAsync.when( data: (accounts) { @@ -416,7 +393,6 @@ final cardColorsProvider = return notifier; }); -// Account-specific color provider final accountCardColorsProvider = StateNotifierProvider.family(( ref, @@ -457,7 +433,7 @@ class CardColorsNotifier extends StateNotifier { final (c1, c2, lightG, darkG) = await CardColorService.load( accountId: accountId, ); - if (currentGeneration != _loadGeneration) return; // stale + if (currentGeneration != _loadGeneration) return; state = CardColors(c1, c2, lightG, darkG); } @@ -467,7 +443,6 @@ class CardColorsNotifier extends StateNotifier { GradientType lightGradient, GradientType darkGradient, ) async { - // Invalidate any in-flight load so it can't overwrite this save. _loadGeneration++; state = CardColors(primary, secondary, lightGradient, darkGradient); await CardColorService.save( @@ -506,20 +481,17 @@ class CardColorsNotifier extends StateNotifier { final previousBrightness = _resolve(previous); final nextBrightness = _resolve(next); - // No change in actual brightness if (previousBrightness == nextBrightness) return; final oldDefaults = _defaultsFor(previousBrightness); final newDefaults = _defaultsFor(nextBrightness); - // Check if current colors match old theme defaults final isUsingOldDefaults = state.primary == oldDefaults.primary && state.secondary == oldDefaults.secondary && state.gradientTypeForBrightness(previousBrightness) == oldDefaults.gradient; - // Only auto-switch if using default colors if (isUsingOldDefaults) { _loadGeneration++; state = CardColors( diff --git a/lib/features/dashboard/screen.dart b/lib/features/dashboard/screen.dart index f9e1b1a..d487873 100644 --- a/lib/features/dashboard/screen.dart +++ b/lib/features/dashboard/screen.dart @@ -44,7 +44,6 @@ class _DashboardScreenState extends ConsumerState { HSVColor savedSecondaryHSV = HSVColor.fromColor( CardColorService.defaultSecondary, ); - // Per-theme gradient types (light/dark), persisted separately. GradientType tempLightGradientType = CardColorService.defaultGradientLight; GradientType tempDarkGradientType = CardColorService.defaultGradientDark; GradientType savedLightGradientType = CardColorService.defaultGradientLight; @@ -52,7 +51,6 @@ class _DashboardScreenState extends ConsumerState { OverlayEntry? overlayEntry; - // Account editing state Account? editingAccount; String tempAccountName = ''; String tempAccountCurrency = 'USD'; @@ -192,8 +190,6 @@ class _DashboardScreenState extends ConsumerState { accountId: newId, ); } else if (editingAccount != null) { - // Existing edit logic - // Save colors await ref .read(accountCardColorsProvider(editingAccount!.id).notifier) .save( @@ -203,7 +199,6 @@ class _DashboardScreenState extends ConsumerState { tempDarkGradientType, ); - // Update account name and currency final updatedAccount = Account( id: editingAccount!.id, name: tempAccountName.trim(), @@ -216,7 +211,6 @@ class _DashboardScreenState extends ConsumerState { await ref.read(accountRepositoryProvider).update(updatedAccount); } } else { - // Restore original values on cancel setState(() { tempPrimary = savedPrimary; tempSecondary = savedSecondary; @@ -370,8 +364,8 @@ class _DashboardScreenState extends ConsumerState { previewSecondary: editingCard ? tempSecondary : null, previewGradientType: editingCard ? (Theme.of(context).brightness == Brightness.dark - ? tempDarkGradientType - : tempLightGradientType) + ? tempDarkGradientType + : tempLightGradientType) : null, ), const SizedBox(height: 16), @@ -457,9 +451,9 @@ class _AccountsInfoBlock extends ConsumerWidget { final s = ref.watch(stringsProvider); final onSurface = Theme.of(context).colorScheme.onSurface; return Padding( - padding: const EdgeInsets.only(bottom: 60), + padding: const EdgeInsets.only(bottom: 60), child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 260), + constraints: const BoxConstraints(maxWidth: 260), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -484,17 +478,26 @@ class _AccountsInfoBlock extends ConsumerWidget { ], ), const SizedBox(height: 12), - _InfoRow(icon: Icons.swap_horiz_rounded, text: s.accountsInfoBalance), + _InfoRow( + icon: Icons.swap_horiz_rounded, + text: s.accountsInfoBalance, + ), const SizedBox(height: 8), - _InfoRow(icon: Icons.touch_app_rounded, text: s.accountsInfoCustomize), + _InfoRow( + icon: Icons.touch_app_rounded, + text: s.accountsInfoCustomize, + ), const SizedBox(height: 8), - _InfoRow(icon: Icons.lock_outline_rounded, text: s.accountsInfoLimit), + _InfoRow( + icon: Icons.lock_outline_rounded, + text: s.accountsInfoLimit, + ), ], ), ), ); } - } +} class _InfoRow extends StatelessWidget { final IconData icon; diff --git a/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart index 35a6041..1497823 100644 --- a/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart +++ b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart @@ -90,7 +90,6 @@ class _AccountEditorOverlayState extends State { final editorPanelTop = cardTop + cardHeight + 20; final colorPanelTop = editorPanelTop + editorPanelHeight + 12; const colorPanelHeight = 410.0; - // Preview card in overlay should match BalanceCardCarousel sizing. return Consumer( builder: (context, ref, _) { @@ -203,25 +202,24 @@ class _AccountEditorOverlayState extends State { child: SizedBox( height: cardHeight, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: BalanceCard( - balance: previewBalance, - currencyInfo: CurrencyInfo( - currencyMap[dash.tempAccountCurrency]?.symbol ?? - '\$', - dash.tempAccountCurrency, - ), - onLongPress: null, - accountName: dash.tempAccountName, - previewPrimary: dash.tempPrimary, - previewSecondary: dash.tempSecondary, - previewGradientType: - Theme.of(widget.context).brightness == - Brightness.dark - ? dash.tempDarkGradientType - : dash.tempLightGradientType, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: BalanceCard( + balance: previewBalance, + currencyInfo: CurrencyInfo( + currencyMap[dash.tempAccountCurrency]?.symbol ?? '\$', + dash.tempAccountCurrency, ), + onLongPress: null, + accountName: dash.tempAccountName, + previewPrimary: dash.tempPrimary, + previewSecondary: dash.tempSecondary, + previewGradientType: + Theme.of(widget.context).brightness == + Brightness.dark + ? dash.tempDarkGradientType + : dash.tempLightGradientType, ), + ), ), ), ), @@ -299,75 +297,75 @@ class _AccountEditorOverlayState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: kDisplayCurrencies.map((entry) { - final isSelected = entry.$1 == _selectedCurrency; - return InkWell( - onTap: () { - setState(() { - _selectedCurrency = entry.$1; - dash.setState(() { - dash.tempAccountCurrency = entry.$1; - }); - dash.overlayEntry?.markNeedsBuild(); - _showCurrencyDropdown = false; - }); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 8, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - entry.$1 == 'BYN' - ? BynSign( - fontSize: 14, - color: isSelected - ? const Color(0xFF7C6DED) - : Theme.of( - widget.context, - ).colorScheme.onSurface, - ) - : Text( - entry.$2, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: isSelected - ? const Color(0xFF7C6DED) - : null, - ), - ), - const SizedBox(width: 4), - Flexible( - child: Text( - entry.$1, + final isSelected = entry.$1 == _selectedCurrency; + return InkWell( + onTap: () { + setState(() { + _selectedCurrency = entry.$1; + dash.setState(() { + dash.tempAccountCurrency = entry.$1; + }); + dash.overlayEntry?.markNeedsBuild(); + _showCurrencyDropdown = false; + }); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + entry.$1 == 'BYN' + ? BynSign( + fontSize: 14, + color: isSelected + ? const Color(0xFF7C6DED) + : Theme.of( + widget.context, + ).colorScheme.onSurface, + ) + : Text( + entry.$2, style: TextStyle( - fontSize: 11, + fontSize: 14, + fontWeight: FontWeight.w600, color: isSelected ? const Color(0xFF7C6DED) - : Theme.of(widget.context) - .colorScheme - .onSurface - .withOpacity(0.6), + : null, ), - overflow: TextOverflow.ellipsis, ), + const SizedBox(width: 4), + Flexible( + child: Text( + entry.$1, + style: TextStyle( + fontSize: 11, + color: isSelected + ? const Color(0xFF7C6DED) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.6), ), - if (isSelected) ...[ - const SizedBox(width: 4), - const Icon( - Icons.check_rounded, - size: 14, - color: Color(0xFF7C6DED), - ), - ], - ], + overflow: TextOverflow.ellipsis, + ), ), - ), - ); - }).toList(), + if (isSelected) ...[ + const SizedBox(width: 4), + const Icon( + Icons.check_rounded, + size: 14, + color: Color(0xFF7C6DED), + ), + ], + ], + ), + ), + ); + }).toList(), ), ), ), diff --git a/lib/features/dashboard/widgets/balance_card_carousel.dart b/lib/features/dashboard/widgets/balance_card_carousel.dart index 99683e9..1e11bdd 100644 --- a/lib/features/dashboard/widgets/balance_card_carousel.dart +++ b/lib/features/dashboard/widgets/balance_card_carousel.dart @@ -41,7 +41,6 @@ class _BalanceCardCarouselState extends ConsumerState { @override void initState() { super.initState(); - // 0.92 позволяет видеть края предыдущей/следующей карточки final savedIndex = ref.read(activeAccountIndexProvider); _pageController = PageController( viewportFraction: 0.92, @@ -68,14 +67,12 @@ class _BalanceCardCarouselState extends ConsumerState { children: [ SizedBox( height: 230, - // OverflowBox позволяет PageView игнорировать паддинги родителя (DashboardScreen) - // и растянуться на всю ширину экрана child: OverflowBox( maxWidth: MediaQuery.of(context).size.width, child: PageView.builder( controller: _pageController, clipBehavior: - Clip.none, // Не обрезает карточку при 3D-наклоне + Clip.none, itemCount: totalPages, onPageChanged: (index) { ref.read(activeAccountIndexProvider.notifier).state = index; @@ -105,7 +102,6 @@ class _BalanceCardCarouselState extends ConsumerState { accountCardColorsProvider(account.id), ); - // Calculate this specific account's balance final txs = ref.watch(transactionsProvider).valueOrNull ?? []; final accountTxs = txs @@ -119,7 +115,7 @@ class _BalanceCardCarouselState extends ConsumerState { t.amount, t.currencyCode, account - .currency, // target is the account's own currency + .currency, ); return t.type == TransactionType.income ? sum + converted @@ -128,7 +124,7 @@ class _BalanceCardCarouselState extends ConsumerState { cardWidget = BalanceCard( balance: - accountBalance, // Use the dynamically calculated balance! + accountBalance, currencyInfo: CurrencyInfo( currencyMap[account.currency]?.symbol ?? '\$', account.currency, @@ -144,7 +140,6 @@ class _BalanceCardCarouselState extends ConsumerState { ); } - // Отступ между карточками во время свайпа return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: cardWidget, @@ -198,12 +193,12 @@ class AddAccountCard extends StatelessWidget { margin: const EdgeInsets.symmetric( horizontal: 10, vertical: 10, - ), // Reduced margins for larger size + ), child: CustomPaint( painter: _DashedBorderPainter(), child: Container( width: double.infinity, - height: 205, // Increased height + height: 205, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface.withOpacity(0.4), borderRadius: BorderRadius.circular(20), @@ -213,7 +208,7 @@ class AddAccountCard extends StatelessWidget { children: [ Icon( Icons.add_rounded, - size: 36, // Slightly bigger icon + size: 36, color: Theme.of( context, ).colorScheme.onSurface.withOpacity(0.5), diff --git a/lib/features/dashboard/widgets/color_editor_overlay.dart b/lib/features/dashboard/widgets/color_editor_overlay.dart index 9d33619..fc34cc1 100644 --- a/lib/features/dashboard/widgets/color_editor_overlay.dart +++ b/lib/features/dashboard/widgets/color_editor_overlay.dart @@ -94,7 +94,6 @@ class _FullScreenBlurOverlayState extends State { child: _buildPanel(panelHeight), ), ), - // Close Button - Top Right Positioned( top: cardTop - 20, right: 20, @@ -367,7 +366,7 @@ class _FullScreenBlurOverlayState extends State { ), const SizedBox(height: 8), SizedBox( - height: 36, // + height: 36, child: ColorPickerSlider( TrackType.hue, currentHSV, diff --git a/lib/features/dashboard/widgets/transaction_tile.dart b/lib/features/dashboard/widgets/transaction_tile.dart index d8d8daa..a0b9a87 100644 --- a/lib/features/dashboard/widgets/transaction_tile.dart +++ b/lib/features/dashboard/widgets/transaction_tile.dart @@ -42,7 +42,6 @@ class TransactionTile extends ConsumerWidget { ? Icons.swap_horiz_rounded : (AppCategories.icons[transaction.category] ?? Icons.category_rounded); - // Check if we're on Total Balance page final activeAccount = ref.watch(activeAccountProvider); final displayCurrency = activeAccount?.currency ?? ref.watch(currencyProvider).code; @@ -57,19 +56,16 @@ class TransactionTile extends ConsumerWidget { : 0.0; final displaySymbol = currencyMap[displayCurrency]?.symbol ?? ''; - // Look up the account name by matching transaction.accountId final accounts = ref.watch(accountsProvider).valueOrNull ?? []; final txAccount = accounts.firstWhereOrNull( (a) => a.id == transaction.accountId, ); - // Build account label with 10-character limit String accountLabel = txAccount?.name ?? ''; if (accountLabel.length > 10) { accountLabel = '${accountLabel.substring(0, 10)}...'; } - // Transfer pairing logic final pairs = ref.watch(transferPairsProvider); final counterpart = pairs[transaction.id]; @@ -381,11 +377,9 @@ class TransactionTile extends ConsumerWidget { bool isIncome, Account? activeAccount, ) { - // Total Balance view with Transfer expense: no prefix if (isTransfer && activeAccount == null && !isIncome) { return ''; } - // All other cases: show + or − return isIncome ? '+ ' : '\u2212 '; } @@ -396,15 +390,12 @@ class TransactionTile extends ConsumerWidget { Account? activeAccount, Color defaultColor, ) { - // Total Balance view with Transfer expense: neutral color if (isTransfer && activeAccount == null && !isIncome) { return Theme.of(context).colorScheme.onSurface.withOpacity(0.8); } - // Transfer in account view or Total Balance income: use income/expense colors if (isTransfer) { return isIncome ? AppColors.income : AppColors.expense; } - // Non-transfer: use default color return defaultColor; } } diff --git a/lib/shared/services/exchange_rate_service.dart b/lib/shared/services/exchange_rate_service.dart index b7158f4..c192462 100644 --- a/lib/shared/services/exchange_rate_service.dart +++ b/lib/shared/services/exchange_rate_service.dart @@ -4,8 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class ExchangeRateService { static const String _primaryUrl = 'https://open.er-api.com/v6/latest/USD'; - static const String _fallbackUrl = - 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json'; + static const String _fallbackUrl = 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json'; static const String _cacheKey = 'exchange_rates'; static const Map _fallbackRates = { diff --git a/lib/shared/services/storage_service.dart b/lib/shared/services/storage_service.dart index 721c475..9781af9 100644 --- a/lib/shared/services/storage_service.dart +++ b/lib/shared/services/storage_service.dart @@ -16,7 +16,6 @@ class StorageService { StorageService(this._prefs); - /// Load all transactions with error handling Result> loadTransactions() { return resultOf(() { final raw = _prefs.getString(_transactionsKey); @@ -29,13 +28,11 @@ class StorageService { }); } - /// Load transactions (legacy - throws on error) List loadTransactionsUnsafe() { final result = loadTransactions(); return result.getOrDefault([]); } - /// Save transactions with error handling Future> saveTransactions(List transactions) async { return asyncResultOf(() async { final encoded = jsonEncode(transactions.map((t) => t.toJson()).toList()); @@ -43,7 +40,6 @@ class StorageService { }); } - /// Add transaction with error handling Future> addTransaction(Transaction transaction) async { return asyncResultOf(() async { final listResult = loadTransactions(); @@ -57,7 +53,6 @@ class StorageService { }); } - /// Update transaction with error handling Future> updateTransaction(Transaction transaction) async { return asyncResultOf(() async { final listResult = loadTransactions(); @@ -76,7 +71,6 @@ class StorageService { }); } - /// Delete transaction with error handling Future> deleteTransaction(String id) async { return asyncResultOf(() async { final listResult = loadTransactions(); @@ -100,7 +94,6 @@ class StorageService { return _prefs.getDouble(_budgetKey); } - /// Save budget with error handling Future> saveBudget(double? budget) async { return asyncResultOf(() async { if (budget == null) { @@ -196,7 +189,6 @@ class StorageService { } } - /// Process recurring transactions with error handling (returns count) Future> processRecurringTransactionsWithResult() async { return asyncResultOf(() async { final transactionsResult = loadTransactions(); diff --git a/lib/shared/widgets/error_snackbar.dart b/lib/shared/widgets/error_snackbar.dart index fabe8a0..4ed2f42 100644 --- a/lib/shared/widgets/error_snackbar.dart +++ b/lib/shared/widgets/error_snackbar.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../core/utils/result.dart'; -/// Show error snackbar with custom styling void showErrorSnackbar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -28,7 +27,6 @@ void showErrorSnackbar(BuildContext context, String message) { ); } -/// Show success snackbar with custom styling void showSuccessSnackbar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -48,7 +46,6 @@ void showSuccessSnackbar(BuildContext context, String message) { ); } -/// Show warning snackbar with custom styling void showWarningSnackbar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -68,31 +65,25 @@ void showWarningSnackbar(BuildContext context, String message) { ); } -/// Extension to handle Result with UI feedback extension ResultUIExtension on Result { - /// Show snackbar on failure Result showErrorOnFailure(BuildContext context) { onFailure((message) => showErrorSnackbar(context, message)); return this; } - /// Show snackbar on success with custom message Result showSuccessMessage(BuildContext context, String message) { onSuccess((_) => showSuccessSnackbar(context, message)); return this; } } -/// Extension for Future> extension FutureResultUIExtension on Future> { - /// Show snackbar on failure Future> showErrorOnFailure(BuildContext context) async { final result = await this; result.onFailure((message) => showErrorSnackbar(context, message)); return result; } - /// Show snackbar on success with custom message Future> showSuccessMessage( BuildContext context, String message, @@ -102,7 +93,6 @@ extension FutureResultUIExtension on Future> { return result; } - /// Show both success and error messages Future> showFeedback( BuildContext context, { required String successMessage, @@ -115,7 +105,6 @@ extension FutureResultUIExtension on Future> { } } -/// Error dialog widget class ErrorDialog extends StatelessWidget { final String title; final String message;