This commit is contained in:
2026-03-27 12:16:37 +03:00
parent b7047c0ec7
commit 123c7d0eb4
9 changed files with 991 additions and 259 deletions
+1
View File
@@ -19,6 +19,7 @@ class AppStrings {
String get filterAll => _ru ? 'Все' : 'All'; String get filterAll => _ru ? 'Все' : 'All';
String get filterIncome => _ru ? 'Доход' : 'Income'; String get filterIncome => _ru ? 'Доход' : 'Income';
String get filterExpense => _ru ? 'Расход' : 'Expense'; String get filterExpense => _ru ? 'Расход' : 'Expense';
String get filterTransfer => _ru ? 'Перевод' : 'Transfer';
String get filterAllTime => _ru ? 'Всё время' : 'All Time'; String get filterAllTime => _ru ? 'Всё время' : 'All Time';
String get filterMonth => _ru ? 'Месяц' : 'Month'; String get filterMonth => _ru ? 'Месяц' : 'Month';
String get income => _ru ? 'Доход' : 'Income'; String get income => _ru ? 'Доход' : 'Income';
+14 -2
View File
@@ -13,6 +13,7 @@ class AddTransactionState {
final String overrideCurrency; final String overrideCurrency;
final String overrideCurrencyCode; final String overrideCurrencyCode;
final int? selectedAccountId; final int? selectedAccountId;
final int? toAccountId;
const AddTransactionState({ const AddTransactionState({
this.amount, this.amount,
@@ -25,6 +26,7 @@ class AddTransactionState {
this.overrideCurrency = '\$', this.overrideCurrency = '\$',
this.overrideCurrencyCode = 'USD', this.overrideCurrencyCode = 'USD',
this.selectedAccountId, this.selectedAccountId,
this.toAccountId,
}); });
factory AddTransactionState.fromTransaction(Transaction tx) { factory AddTransactionState.fromTransaction(Transaction tx) {
@@ -56,6 +58,7 @@ class AddTransactionState {
String? overrideCurrency, String? overrideCurrency,
String? overrideCurrencyCode, String? overrideCurrencyCode,
int? selectedAccountId, int? selectedAccountId,
int? toAccountId,
}) => AddTransactionState( }) => AddTransactionState(
amount: amount ?? this.amount, amount: amount ?? this.amount,
category: category ?? this.category, category: category ?? this.category,
@@ -67,6 +70,7 @@ class AddTransactionState {
overrideCurrency: overrideCurrency ?? this.overrideCurrency, overrideCurrency: overrideCurrency ?? this.overrideCurrency,
overrideCurrencyCode: overrideCurrencyCode ?? this.overrideCurrencyCode, overrideCurrencyCode: overrideCurrencyCode ?? this.overrideCurrencyCode,
selectedAccountId: selectedAccountId ?? this.selectedAccountId, selectedAccountId: selectedAccountId ?? this.selectedAccountId,
toAccountId: toAccountId ?? this.toAccountId,
); );
bool get isEditing => editingId != null; bool get isEditing => editingId != null;
@@ -85,8 +89,12 @@ class AddTransactionNotifier extends StateNotifier<AddTransactionState> {
void setCategory(String v) => state = state.copyWith(category: v); void setCategory(String v) => state = state.copyWith(category: v);
void setType(TransactionType v) { void setType(TransactionType v) {
final newCategory = AppCategories.forType(v).first; if (v == TransactionType.transfer) {
state = state.copyWith(type: v, category: newCategory); state = state.copyWith(type: v, category: 'Transfer', toAccountId: null);
} else {
final newCategory = AppCategories.forType(v).first;
state = state.copyWith(type: v, category: newCategory, toAccountId: null);
}
} }
void setDate(DateTime v) => state = state.copyWith(date: v); void setDate(DateTime v) => state = state.copyWith(date: v);
@@ -104,6 +112,10 @@ class AddTransactionNotifier extends StateNotifier<AddTransactionState> {
void setAccountId(int id) => state = state.copyWith(selectedAccountId: id); void setAccountId(int id) => state = state.copyWith(selectedAccountId: id);
void setToAccountId(int? id) => state = state.copyWith(toAccountId: id);
bool get isTransfer => state.type == TransactionType.transfer;
void reset() => state = AddTransactionState.empty(); void reset() => state = AddTransactionState.empty();
} }
+295 -91
View File
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -10,6 +9,7 @@ import '../../shared/models/transaction.dart';
import '../dashboard/provider.dart'; import '../dashboard/provider.dart';
import '../settings/provider.dart'; import '../settings/provider.dart';
import 'provider.dart'; import 'provider.dart';
import 'widgets/account_row.dart';
import 'widgets/account_selector.dart'; import 'widgets/account_selector.dart';
import 'widgets/amount_input.dart'; import 'widgets/amount_input.dart';
import 'widgets/category_picker.dart'; import 'widgets/category_picker.dart';
@@ -37,13 +37,17 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _noteController = TextEditingController(); final _noteController = TextEditingController();
final _accountIndicatorKey = GlobalKey(); final _fromAccountIndicatorKey = GlobalKey();
final _toAccountIndicatorKey = GlobalKey();
late AnimationController _shakeController; late AnimationController _shakeController;
late Animation<Color?> _borderColorAnimation; late Animation<Color?> _borderColorAnimation;
bool _showError = false; bool _showError = false;
late DateTime _selectedDate; late DateTime _selectedDate;
late TimeOfDay _selectedTime; late TimeOfDay _selectedTime;
bool _showAccountDropdown = false; bool _showFromAccountDropdown = false;
bool _showToAccountDropdown = false;
String? _toAccountError;
String? _fromAccountError;
@override @override
void initState() { void initState() {
@@ -82,6 +86,13 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
ref ref
.read(addTransactionProvider(null).notifier) .read(addTransactionProvider(null).notifier)
.setCurrency(currencySymbol, currencyCode); .setCurrency(currencySymbol, currencyCode);
// Set the selected account if there's an active account
if (activeAccount != null) {
ref
.read(addTransactionProvider(null).notifier)
.setAccountId(activeAccount.id);
}
}); });
} }
} }
@@ -134,6 +145,30 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
final amount = double.parse(parsed); final amount = double.parse(parsed);
final state = ref.read(addTransactionProvider(widget.initial)); final state = ref.read(addTransactionProvider(widget.initial));
// Validate transfer
if (state.type == TransactionType.transfer) {
bool hasError = false;
if (state.selectedAccountId == null) {
setState(() => _fromAccountError = 'Please select a source account');
hasError = true;
} else {
setState(() => _fromAccountError = null);
}
if (state.toAccountId == null) {
setState(() => _toAccountError = 'Please select a destination account');
hasError = true;
} else if (state.toAccountId == state.selectedAccountId) {
setState(() => _toAccountError = 'Source and destination must differ');
hasError = true;
} else {
setState(() => _toAccountError = null);
}
if (hasError) return;
}
final finalDateTime = DateTime( final finalDateTime = DateTime(
_selectedDate.year, _selectedDate.year,
_selectedDate.month, _selectedDate.month,
@@ -160,56 +195,100 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
'Amount: $amount, Category: ${state.category}, Type: ${state.type.name}', 'Amount: $amount, Category: ${state.category}, Type: ${state.type.name}',
); );
final activeAccount = ref.read(activeAccountProvider); if (state.type == TransactionType.transfer) {
final selectedId = ref // Handle transfer: create two transactions
.read(addTransactionProvider(widget.initial)) // Get currency with fallback to global currency
.selectedAccountId; final curr = ref.read(currencyProvider);
int accountId; final currency = state.overrideCurrency.isNotEmpty
? state.overrideCurrency
: curr.symbol;
final currencyCode = state.overrideCurrencyCode.isNotEmpty
? state.overrideCurrencyCode
: curr.code;
if (selectedId != null && selectedId != 0) { final expense = Transaction(
print('Using selected account ID: $selectedId'); id: _uuid.v4(),
accountId = selectedId; amount: amount,
} else if (activeAccount != null) { category: 'Transfer',
print( type: TransactionType.expense,
'Using active account ID: ${activeAccount.id}, Name: ${activeAccount.name}', date: finalDateTime,
note: note,
currency: currency,
currencyCode: currencyCode,
accountId: state.selectedAccountId!,
); );
accountId = activeAccount.id;
final income = Transaction(
id: _uuid.v4(),
amount: amount,
category: 'Transfer',
type: TransactionType.income,
date: finalDateTime,
note: note,
currency: currency,
currencyCode: currencyCode,
accountId: state.toAccountId!,
);
print('Creating transfer transactions...');
await ref.read(transactionsProvider.notifier).add(expense);
await ref.read(transactionsProvider.notifier).add(income);
print('Transfer completed');
} else { } else {
print('No active account. Fetching main account...'); // Handle regular income/expense
final mainAccount = await ref.read(accountRepositoryProvider).getMain(); final activeAccount = ref.read(activeAccountProvider);
print( final selectedId = ref
'Main account fetched: ID=${mainAccount.id}, Name: ${mainAccount.name}', .read(addTransactionProvider(widget.initial))
); .selectedAccountId;
accountId = mainAccount.id; int accountId;
}
final tx = Transaction( if (selectedId != null && selectedId != 0) {
id: state.editingId ?? _uuid.v4(), print('Using selected account ID: $selectedId');
amount: amount, accountId = selectedId;
category: state.category, } else if (activeAccount != null) {
type: state.type, print(
date: finalDateTime, 'Using active account ID: ${activeAccount.id}, Name: ${activeAccount.name}',
note: note, );
currency: state.overrideCurrency, accountId = activeAccount.id;
currencyCode: state.overrideCurrencyCode, } else {
accountId: accountId, 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;
}
print('Transaction object created: ID=${tx.id}, AccId=${tx.accountId}'); final tx = Transaction(
print('Calling provider to save...'); id: state.editingId ?? _uuid.v4(),
amount: amount,
if (state.isEditing) { category: state.category,
await ref.read(transactionsProvider.notifier).update(tx); type: state.type,
print('Update completed'); date: finalDateTime,
} else { note: note,
final res = await ref.read(transactionsProvider.notifier).add(tx); currency: state.overrideCurrency,
print( currencyCode: state.overrideCurrencyCode,
'Add completed. Result: ${res.isSuccess ? "SUCCESS" : "FAILURE"}', accountId: accountId,
); );
if (res.isFailure) { print('Transaction object created: ID=${tx.id}, AccId=${tx.accountId}');
print('!!! Provider returned failure: ${res.errorOrNull}'); print('Calling provider to save...');
throw Exception(res.errorOrNull);
if (state.isEditing) {
await ref.read(transactionsProvider.notifier).update(tx);
print('Update completed');
} else {
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);
}
} }
} }
@@ -349,37 +428,32 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
ListView( ListView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
children: [ children: [
Row( TypeToggle(
children: [ selected: state.type,
Expanded( onChanged: (type) => ref
child: Column( .read(addTransactionProvider(widget.initial).notifier)
crossAxisAlignment: CrossAxisAlignment.start, .setType(type),
children: [ isDark: isDark,
AccountSelector( ),
initial: widget.initial, const SizedBox(height: 16),
showDropdown: _showAccountDropdown,
onToggleDropdown: () => setState( AccountRow(
() => _showAccountDropdown = initial: widget.initial,
!_showAccountDropdown, showFromDropdown: _showFromAccountDropdown,
), showToDropdown: _showToAccountDropdown,
indicatorKey: _accountIndicatorKey, onToggleFromDropdown: () => setState(() {
), _showFromAccountDropdown = !_showFromAccountDropdown;
], _fromAccountError = null;
), }),
), onToggleToDropdown: () => setState(() {
const SizedBox(width: 12), _showToAccountDropdown = !_showToAccountDropdown;
Expanded( _toAccountError = null;
child: TypeToggle( }),
selected: state.type, fromIndicatorKey: _fromAccountIndicatorKey,
onChanged: (type) => ref toIndicatorKey: _toAccountIndicatorKey,
.read( fromAccountError: _fromAccountError,
addTransactionProvider(widget.initial).notifier, toAccountError: _toAccountError,
) isDark: isDark,
.setType(type),
isDark: isDark,
),
),
],
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -419,16 +493,18 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
SectionLabel(s.category), if (state.type != TransactionType.transfer) ...[
const SizedBox(height: 8), SectionLabel(s.category),
CategoryPicker( const SizedBox(height: 8),
categories: categories, CategoryPicker(
selected: state.category, categories: categories,
onChanged: (c) => ref selected: state.category,
.read(addTransactionProvider(widget.initial).notifier) onChanged: (c) => ref
.setCategory(c), .read(addTransactionProvider(widget.initial).notifier)
), .setCategory(c),
const SizedBox(height: 20), ),
const SizedBox(height: 20),
],
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -477,18 +553,33 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
), ),
], ],
), ),
if (_showAccountDropdown) if (_showFromAccountDropdown)
Positioned.fill( Positioned.fill(
child: GestureDetector( child: GestureDetector(
onTap: () => setState(() => _showAccountDropdown = false), onTap: () =>
setState(() => _showFromAccountDropdown = false),
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: const SizedBox.expand(), child: const SizedBox.expand(),
), ),
), ),
if (_showAccountDropdown) if (_showFromAccountDropdown)
AccountDropdownOverlay( AccountDropdownOverlay(
initial: widget.initial, initial: widget.initial,
onClose: () => setState(() => _showAccountDropdown = false), onClose: () =>
setState(() => _showFromAccountDropdown = false),
),
if (_showToAccountDropdown)
Positioned.fill(
child: GestureDetector(
onTap: () => setState(() => _showToAccountDropdown = false),
behavior: HitTestBehavior.translucent,
child: const SizedBox.expand(),
),
),
if (_showToAccountDropdown)
_ToAccountDropdownOverlay(
initial: widget.initial,
onClose: () => setState(() => _showToAccountDropdown = false),
), ),
], ],
), ),
@@ -497,3 +588,116 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
); );
} }
} }
class _ToAccountDropdownOverlay extends ConsumerWidget {
final Transaction? initial;
final VoidCallback onClose;
const _ToAccountDropdownOverlay({
required this.initial,
required this.onClose,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final accountsAsync = ref.watch(accountsProvider);
final selectedAccountId = ref
.read(addTransactionProvider(initial))
.selectedAccountId;
final toAccountId = ref.read(addTransactionProvider(initial)).toAccountId;
return Positioned(
top: 340,
left: 20,
right: 20,
child: Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF7C6DED).withOpacity(0.3),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: accountsAsync.when(
data: (accounts) {
final filteredAccounts = accounts
.where((a) => a.id != selectedAccountId)
.toList();
return Column(
mainAxisSize: MainAxisSize.min,
children: filteredAccounts.map((account) {
final isSelected = account.id == toAccountId;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
ref
.read(addTransactionProvider(initial).notifier)
.setToAccountId(account.id);
onClose();
HapticService.light();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_rounded,
size: 16,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
const SizedBox(width: 10),
Expanded(
child: Text(
account.name,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w400,
color: isSelected
? const Color(0xFF7C6DED)
: Theme.of(context).colorScheme.onSurface,
),
),
),
if (isSelected)
const Icon(
Icons.check_rounded,
size: 16,
color: Color(0xFF7C6DED),
),
],
),
),
);
}).toList(),
);
},
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
),
),
),
);
}
}
@@ -0,0 +1,425 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../shared/models/account.dart';
import '../../../shared/models/transaction.dart';
import '../../dashboard/provider.dart';
import '../provider.dart';
class AccountRow extends ConsumerWidget {
final Transaction? initial;
final bool showFromDropdown;
final bool showToDropdown;
final VoidCallback onToggleFromDropdown;
final VoidCallback onToggleToDropdown;
final GlobalKey fromIndicatorKey;
final GlobalKey toIndicatorKey;
final String? fromAccountError;
final String? toAccountError;
final bool isDark;
const AccountRow({
super.key,
required this.initial,
required this.showFromDropdown,
required this.showToDropdown,
required this.onToggleFromDropdown,
required this.onToggleToDropdown,
required this.fromIndicatorKey,
required this.toIndicatorKey,
this.fromAccountError,
this.toAccountError,
required this.isDark,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(addTransactionProvider(initial));
final accountsAsync = ref.watch(accountsProvider);
final accounts = accountsAsync.valueOrNull ?? [];
final isTransfer = state.type == TransactionType.transfer;
// Auto-select toAccount when only 2 accounts exist
if (isTransfer && accounts.length == 2 && state.selectedAccountId != null) {
final otherId = accounts
.firstWhere(
(a) => a.id != state.selectedAccountId,
orElse: () => accounts.first,
)
.id;
if (state.toAccountId != otherId) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref
.read(addTransactionProvider(initial).notifier)
.setToAccountId(otherId);
});
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Account',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 8),
if (isTransfer)
_TransferAccountRow(
initial: initial,
accounts: accounts,
showFromDropdown: showFromDropdown,
showToDropdown: showToDropdown,
onToggleFromDropdown: onToggleFromDropdown,
onToggleToDropdown: onToggleToDropdown,
fromIndicatorKey: fromIndicatorKey,
toIndicatorKey: toIndicatorKey,
fromAccountError: fromAccountError,
toAccountError: toAccountError,
isDark: isDark,
)
else
_SingleAccountSelector(
initial: initial,
accounts: accounts,
showDropdown: showFromDropdown,
onToggleDropdown: onToggleFromDropdown,
indicatorKey: fromIndicatorKey,
error: fromAccountError,
isDark: isDark,
),
],
);
}
}
class _SingleAccountSelector extends ConsumerWidget {
final Transaction? initial;
final List<Account> accounts;
final bool showDropdown;
final VoidCallback onToggleDropdown;
final GlobalKey indicatorKey;
final String? error;
final bool isDark;
const _SingleAccountSelector({
required this.initial,
required this.accounts,
required this.showDropdown,
required this.onToggleDropdown,
required this.indicatorKey,
this.error,
required this.isDark,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(addTransactionProvider(initial));
final activeAccount = ref.watch(activeAccountProvider);
final selectedAccountId = state.selectedAccountId;
final Account? displayAccount;
if (selectedAccountId != null) {
displayAccount = accounts.firstWhere(
(a) => a.id == selectedAccountId,
orElse: () => accounts.isNotEmpty
? accounts.first
: Account(
id: 0,
name: '',
currency: 'USD',
isMain: false,
sortOrder: 0,
createdAt: DateTime.now(),
),
);
} else {
displayAccount =
activeAccount ?? (accounts.isNotEmpty ? accounts.first : null);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: onToggleDropdown,
child: Container(
key: indicatorKey,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
color: error != null
? const Color(0xFFE05C6B).withOpacity(0.1)
: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: error != null
? const Color(0xFFE05C6B)
: isDark
? Colors.white.withOpacity(0.1)
: const Color(0xFFDDDDEE),
width: error != null ? 1.5 : 1,
),
),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_rounded,
size: 18,
color: error != null
? const Color(0xFFE05C6B)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
const SizedBox(width: 10),
Expanded(
child: Text(
displayAccount?.name ?? 'Select account',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: displayAccount != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.4),
fontWeight: FontWeight.w500,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
Icon(
showDropdown ? Icons.arrow_drop_up : Icons.arrow_drop_down,
size: 20,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
],
),
),
),
if (error != null) ...[
const SizedBox(height: 6),
Text(
error!,
style: const TextStyle(fontSize: 12, color: Color(0xFFE05C6B)),
),
],
],
);
}
}
class _TransferAccountRow extends ConsumerWidget {
final Transaction? initial;
final List<Account> accounts;
final bool showFromDropdown;
final bool showToDropdown;
final VoidCallback onToggleFromDropdown;
final VoidCallback onToggleToDropdown;
final GlobalKey fromIndicatorKey;
final GlobalKey toIndicatorKey;
final String? fromAccountError;
final String? toAccountError;
final bool isDark;
const _TransferAccountRow({
required this.initial,
required this.accounts,
required this.showFromDropdown,
required this.showToDropdown,
required this.onToggleFromDropdown,
required this.onToggleToDropdown,
required this.fromIndicatorKey,
required this.toIndicatorKey,
this.fromAccountError,
this.toAccountError,
required this.isDark,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(addTransactionProvider(initial));
final activeAccount = ref.watch(activeAccountProvider);
final selectedAccountId = state.selectedAccountId;
final toAccountId = state.toAccountId;
final Account? fromAccount;
if (selectedAccountId != null) {
fromAccount = accounts.firstWhere(
(a) => a.id == selectedAccountId,
orElse: () => accounts.isNotEmpty
? accounts.first
: Account(
id: 0,
name: '',
currency: 'USD',
isMain: false,
sortOrder: 0,
createdAt: DateTime.now(),
),
);
} else {
fromAccount =
activeAccount ?? (accounts.isNotEmpty ? accounts.first : null);
}
final Account? toAccount = toAccountId != null && accounts.isNotEmpty
? accounts.firstWhere(
(a) => a.id == toAccountId,
orElse: () => accounts.first,
)
: null;
final autoSelectEnabled = accounts.length == 2;
return Row(
children: [
Expanded(
child: _AccountHalf(
account: fromAccount,
label: 'From',
showDropdown: showFromDropdown,
onToggle: onToggleFromDropdown,
indicatorKey: fromIndicatorKey,
error: fromAccountError,
isDark: isDark,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Icon(
Icons.swap_horiz_rounded,
size: 24,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
),
),
Expanded(
child: _AccountHalf(
account: toAccount,
label: 'To',
showDropdown: showToDropdown,
onToggle: autoSelectEnabled ? null : onToggleToDropdown,
indicatorKey: toIndicatorKey,
error: toAccountError,
isDark: isDark,
disabled: autoSelectEnabled,
),
),
],
);
}
}
class _AccountHalf extends StatelessWidget {
final Account? account;
final String label;
final bool showDropdown;
final VoidCallback? onToggle;
final GlobalKey indicatorKey;
final String? error;
final bool isDark;
final bool disabled;
const _AccountHalf({
required this.account,
required this.label,
required this.showDropdown,
required this.onToggle,
required this.indicatorKey,
this.error,
required this.isDark,
this.disabled = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: disabled ? null : onToggle,
child: Container(
key: indicatorKey,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: error != null
? const Color(0xFFE05C6B).withOpacity(0.1)
: disabled
? Theme.of(context).colorScheme.surface.withOpacity(0.5)
: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: error != null
? const Color(0xFFE05C6B)
: isDark
? Colors.white.withOpacity(disabled ? 0.05 : 0.1)
: const Color(0xFFDDDDEE),
width: error != null ? 1.5 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
label,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface
.withOpacity(disabled ? 0.3 : 0.5),
fontWeight: FontWeight.w500,
),
),
const Spacer(),
if (onToggle != null)
Icon(
showDropdown
? Icons.arrow_drop_up
: Icons.arrow_drop_down,
size: 16,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.5),
),
],
),
const SizedBox(height: 2),
Text(
account?.name ?? 'Select',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: account != null
? Theme.of(context).colorScheme.onSurface.withOpacity(
disabled ? 0.5 : 1.0,
)
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.4),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
),
if (error != null) ...[
const SizedBox(height: 4),
Text(
error!,
style: const TextStyle(fontSize: 11, color: Color(0xFFE05C6B)),
),
],
],
);
}
}
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants.dart'; import '../../../core/constants.dart';
import '../../../shared/models/transaction.dart'; import '../../../shared/models/transaction.dart';
import '../../dashboard/provider.dart';
class TypeToggle extends StatelessWidget { class TypeToggle extends ConsumerWidget {
final TransactionType selected; final TransactionType selected;
final ValueChanged<TransactionType> onChanged; final ValueChanged<TransactionType> onChanged;
final bool isDark; final bool isDark;
@@ -15,7 +17,11 @@ class TypeToggle extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final accountsAsync = ref.watch(accountsProvider);
final accounts = accountsAsync.valueOrNull ?? [];
final transferDisabled = accounts.length <= 1;
return Container( return Container(
height: 56, height: 56,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -28,53 +34,36 @@ class TypeToggle extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: GestureDetector( child: _TypeOption(
icon: Icons.arrow_downward_rounded,
label: 'Income',
color: AppColors.income,
isSelected: selected == TransactionType.income,
onTap: () => onChanged(TransactionType.income), onTap: () => onChanged(TransactionType.income),
child: AnimatedContainer( isDark: isDark,
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: selected == TransactionType.income
? AppColors.income.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(11),
),
child: Center(
child: Icon(
Icons.arrow_downward_rounded,
color: selected == TransactionType.income
? AppColors.income
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.4),
size: 20,
),
),
),
), ),
), ),
Expanded( Expanded(
child: GestureDetector( child: _TypeOption(
icon: Icons.arrow_upward_rounded,
label: 'Expense',
color: AppColors.expense,
isSelected: selected == TransactionType.expense,
onTap: () => onChanged(TransactionType.expense), onTap: () => onChanged(TransactionType.expense),
child: AnimatedContainer( isDark: isDark,
duration: const Duration(milliseconds: 200), ),
decoration: BoxDecoration( ),
color: selected == TransactionType.expense Expanded(
? AppColors.expense.withOpacity(0.15) child: _TypeOption(
: Colors.transparent, icon: Icons.swap_horiz_rounded,
borderRadius: BorderRadius.circular(11), label: 'Transfer',
), color: Colors.blueAccent,
child: Center( isSelected: selected == TransactionType.transfer,
child: Icon( onTap: transferDisabled
Icons.arrow_upward_rounded, ? null
color: selected == TransactionType.expense : () => onChanged(TransactionType.transfer),
? AppColors.expense isDark: isDark,
: Theme.of( disabled: transferDisabled,
context,
).colorScheme.onSurface.withOpacity(0.4),
size: 20,
),
),
),
), ),
), ),
], ],
@@ -82,3 +71,75 @@ class TypeToggle extends StatelessWidget {
); );
} }
} }
class _TypeOption extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
final bool isSelected;
final VoidCallback? onTap;
final bool isDark;
final bool disabled;
const _TypeOption({
required this.icon,
required this.label,
required this.color,
required this.isSelected,
required this.onTap,
required this.isDark,
this.disabled = false,
});
@override
Widget build(BuildContext context) {
final effectiveColor = disabled
? Theme.of(context).colorScheme.onSurface.withOpacity(0.2)
: color;
return GestureDetector(
onTap: disabled ? null : onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isSelected && !disabled
? effectiveColor.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(11),
border: isSelected && !disabled
? Border.all(color: effectiveColor, width: 1.5)
: null,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: isSelected && !disabled
? effectiveColor
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(disabled ? 0.2 : 0.4),
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected && !disabled
? effectiveColor
: Theme.of(context).colorScheme.onSurface.withOpacity(
disabled ? 0.2 : 0.5,
),
),
),
],
),
),
),
);
}
}
+14 -7
View File
@@ -114,7 +114,7 @@ class TransactionsNotifier
final searchQueryProvider = StateProvider<String>((ref) => ''); final searchQueryProvider = StateProvider<String>((ref) => '');
enum TransactionFilter { all, income, expense } enum TransactionFilter { all, income, expense, transfer }
enum TimeFilter { allTime, lastMonth } enum TimeFilter { allTime, lastMonth }
@@ -146,7 +146,7 @@ final globalTotalBalanceProvider = Provider<double>((ref) {
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
final targetCurrency = ref.watch(currencyProvider).code; final targetCurrency = ref.watch(currencyProvider).code;
return txs.fold(0.0, (sum, t) { return txs.where((t) => t.category != 'Transfer').fold(0.0, (sum, t) {
final converted = exchangeService.convert( final converted = exchangeService.convert(
t.amount, t.amount,
t.currencyCode, t.currencyCode,
@@ -173,7 +173,7 @@ final totalBalanceProvider = Provider<double>((ref) {
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
return txs.fold(0.0, (sum, t) { return txs.where((t) => t.category != 'Transfer').fold(0.0, (sum, t) {
final converted = exchangeService.convert( final converted = exchangeService.convert(
t.amount, t.amount,
t.currencyCode, t.currencyCode,
@@ -186,7 +186,9 @@ final totalBalanceProvider = Provider<double>((ref) {
final totalIncomeProvider = Provider<double>((ref) { final totalIncomeProvider = Provider<double>((ref) {
// Watch the filtered transactions directly // Watch the filtered transactions directly
final txs = ref.watch(accountFilteredTransactionsProvider); final txs = ref.watch(accountFilteredTransactionsProvider);
final filtered = txs.where((t) => t.type == TransactionType.income); final filtered = txs.where(
(t) => t.type == TransactionType.income && t.category != 'Transfer',
);
// Watch the dependencies that change on swipe! // Watch the dependencies that change on swipe!
final index = ref.watch(activeAccountIndexProvider); final index = ref.watch(activeAccountIndexProvider);
@@ -212,7 +214,9 @@ final totalIncomeProvider = Provider<double>((ref) {
final totalExpenseProvider = Provider<double>((ref) { final totalExpenseProvider = Provider<double>((ref) {
final txs = ref.watch(accountFilteredTransactionsProvider); final txs = ref.watch(accountFilteredTransactionsProvider);
final filtered = txs.where((t) => t.type == TransactionType.expense); final filtered = txs.where(
(t) => t.type == TransactionType.expense && t.category != 'Transfer',
);
final index = ref.watch(activeAccountIndexProvider); final index = ref.watch(activeAccountIndexProvider);
final accountsAsync = ref.watch(accountsProvider); final accountsAsync = ref.watch(accountsProvider);
@@ -291,6 +295,8 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
filtered = filtered filtered = filtered
.where((t) => t.type == TransactionType.expense) .where((t) => t.type == TransactionType.expense)
.toList(); .toList();
} else if (typeFilter == TransactionFilter.transfer) {
filtered = filtered.where((t) => t.category == 'Transfer').toList();
} }
if (query.isNotEmpty) { if (query.isNotEmpty) {
@@ -402,8 +408,9 @@ class CardColorsNotifier extends StateNotifier<CardColors> {
Future<void> _load() async { Future<void> _load() async {
final currentGeneration = ++_loadGeneration; final currentGeneration = ++_loadGeneration;
final (c1, c2, lightG, darkG) = final (c1, c2, lightG, darkG) = await CardColorService.load(
await CardColorService.load(accountId: accountId); accountId: accountId,
);
if (currentGeneration != _loadGeneration) return; // stale if (currentGeneration != _loadGeneration) return; // stale
state = CardColors(c1, c2, lightG, darkG); state = CardColors(c1, c2, lightG, darkG);
} }
@@ -15,49 +15,66 @@ class FilterChips extends ConsumerWidget {
final timeFilter = ref.watch(timeFilterProvider); final timeFilter = ref.watch(timeFilterProvider);
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
return Row( return SingleChildScrollView(
children: [ scrollDirection: Axis.horizontal,
_FilterChip( clipBehavior: Clip.none,
label: strings.filterAllTime, child: Row(
isSelected: timeFilter == TimeFilter.allTime, children: [
onTap: () => ref.read(timeFilterProvider.notifier).state = TimeFilter.allTime, _FilterChip(
), label: strings.filterAllTime,
const SizedBox(width: 6), isSelected: timeFilter == TimeFilter.allTime,
_FilterChip( onTap: () => ref.read(timeFilterProvider.notifier).state =
label: strings.filterMonth, TimeFilter.allTime,
isSelected: timeFilter == TimeFilter.lastMonth, ),
onTap: () => ref.read(timeFilterProvider.notifier).state = TimeFilter.lastMonth, const SizedBox(width: 6),
), _FilterChip(
Padding( label: strings.filterMonth,
padding: const EdgeInsets.symmetric(horizontal: 8), isSelected: timeFilter == TimeFilter.lastMonth,
child: Container( onTap: () => ref.read(timeFilterProvider.notifier).state =
width: 1, TimeFilter.lastMonth,
height: 20, ),
color: Theme.of(context).colorScheme.onSurface.withOpacity( Padding(
isDark ? 0.15 : 0.2, padding: const EdgeInsets.symmetric(horizontal: 8),
child: Container(
width: 1,
height: 20,
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(isDark ? 0.15 : 0.2),
), ),
), ),
), _FilterChip(
_FilterChip( label: strings.filterAll,
label: strings.filterAll, isSelected: typeFilter == TransactionFilter.all,
isSelected: typeFilter == TransactionFilter.all, onTap: () => ref.read(transactionFilterProvider.notifier).state =
onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.all, TransactionFilter.all,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
_FilterChip( _FilterChip(
label: strings.filterIncome, label: strings.filterIncome,
isSelected: typeFilter == TransactionFilter.income, isSelected: typeFilter == TransactionFilter.income,
color: AppColors.income, color: AppColors.income,
onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.income, onTap: () => ref.read(transactionFilterProvider.notifier).state =
), TransactionFilter.income,
const SizedBox(width: 6), ),
_FilterChip( const SizedBox(width: 6),
label: strings.filterExpense, _FilterChip(
isSelected: typeFilter == TransactionFilter.expense, label: strings.filterExpense,
color: AppColors.expense, isSelected: typeFilter == TransactionFilter.expense,
onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.expense, color: AppColors.expense,
), onTap: () => ref.read(transactionFilterProvider.notifier).state =
], TransactionFilter.expense,
),
const SizedBox(width: 6),
_FilterChip(
label: strings.filterTransfer,
isSelected: typeFilter == TransactionFilter.transfer,
color: Colors.blueAccent,
onTap: () => ref.read(transactionFilterProvider.notifier).state =
TransactionFilter.transfer,
),
],
),
); );
} }
} }
@@ -96,8 +113,8 @@ class _FilterChip extends StatelessWidget {
border: isSelected border: isSelected
? Border.all(color: chipColor, width: 1.5) ? Border.all(color: chipColor, width: 1.5)
: isDark : isDark
? null ? null
: Border.all(color: const Color(0xFFDDDDEE), width: 1), : Border.all(color: const Color(0xFFDDDDEE), width: 1),
), ),
child: Text( child: Text(
label, label,
@@ -29,19 +29,23 @@ class TransactionTile extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(stringsProvider); final s = ref.watch(stringsProvider);
final fmt = ref.watch(amountFormatProvider); final fmt = ref.watch(amountFormatProvider);
final isTransfer = transaction.category == 'Transfer';
final isIncome = transaction.type == TransactionType.income; final isIncome = transaction.type == TransactionType.income;
final color = isIncome ? AppColors.income : AppColors.expense; final color = isTransfer
final catColor = ? const Color(0xFF7C6DED)
AppCategories.colors[transaction.category] ?? AppColors.accent; : (isIncome ? AppColors.income : AppColors.expense);
final catIcon = final catColor = isTransfer
AppCategories.icons[transaction.category] ?? Icons.category_rounded; ? const Color(0xFF7C6DED)
: (AppCategories.colors[transaction.category] ?? AppColors.accent);
final catIcon = isTransfer
? Icons.swap_horiz_rounded
: (AppCategories.icons[transaction.category] ?? Icons.category_rounded);
// Check if we're on Total Balance page // Check if we're on Total Balance page
final activeAccount = ref.watch(activeAccountProvider); final activeAccount = ref.watch(activeAccountProvider);
final displayCurrency = activeAccount?.currency ?? final displayCurrency =
ref.watch(currencyProvider).code; activeAccount?.currency ?? ref.watch(currencyProvider).code;
final showConverted = final showConverted = transaction.currencyCode != displayCurrency;
transaction.currencyCode != displayCurrency;
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
final convertedAmount = showConverted final convertedAmount = showConverted
? exchangeService.convert( ? exchangeService.convert(
@@ -50,8 +54,7 @@ class TransactionTile extends ConsumerWidget {
displayCurrency, displayCurrency,
) )
: 0.0; : 0.0;
final displaySymbol = final displaySymbol = currencyMap[displayCurrency]?.symbol ?? '';
currencyMap[displayCurrency]?.symbol ?? '';
// Look up the account name by matching transaction.accountId // Look up the account name by matching transaction.accountId
final accounts = ref.watch(accountsProvider).valueOrNull ?? []; final accounts = ref.watch(accountsProvider).valueOrNull ?? [];
@@ -93,7 +96,9 @@ class TransactionTile extends ConsumerWidget {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
s.categoryLabel(transaction.category), isTransfer
? 'Transfer'
: s.categoryLabel(transaction.category),
style: Theme.of(context).textTheme.bodyMedium style: Theme.of(context).textTheme.bodyMedium
?.copyWith( ?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -143,12 +148,12 @@ class TransactionTile extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
isIncome ? '+ ' : '- ', isTransfer ? '' : (isIncome ? '+ ' : '- '),
style: Theme.of(context).textTheme.bodyMedium style: Theme.of(context).textTheme.bodyMedium
?.copyWith( ?.copyWith(
color: color, color: color,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
BynSign(fontSize: 14, color: color), BynSign(fontSize: 14, color: color),
const SizedBox(width: 2), const SizedBox(width: 2),
@@ -156,14 +161,20 @@ class TransactionTile extends ConsumerWidget {
formatAmount('', transaction.amount, fmt), formatAmount('', transaction.amount, fmt),
style: Theme.of(context).textTheme.bodyMedium style: Theme.of(context).textTheme.bodyMedium
?.copyWith( ?.copyWith(
color: color, color: color,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
], ],
) )
: Text( : Text(
'${isIncome ? '+ ' : '- '}${formatAmount(transaction.currency, transaction.amount, fmt)}', isTransfer
? formatAmount(
transaction.currency,
transaction.amount,
fmt,
)
: '${isIncome ? '+ ' : '- '}${formatAmount(transaction.currency, transaction.amount, fmt)}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: color, color: color,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -184,10 +195,7 @@ class TransactionTile extends ConsumerWidget {
height: 1.3, height: 1.3,
), ),
), ),
BynSign( BynSign(fontSize: 11, color: color.withOpacity(0.5)),
fontSize: 11,
color: color.withOpacity(0.5),
),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
formatAmount('', convertedAmount, fmt), formatAmount('', convertedAmount, fmt),
+45 -48
View File
@@ -1,6 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
enum TransactionType { income, expense } enum TransactionType { income, expense, transfer }
enum RecurrenceType { none, daily, weekly, monthly } enum RecurrenceType { none, daily, weekly, monthly }
@@ -33,41 +33,39 @@ class Transaction {
}); });
factory Transaction.fromJson(Map<String, dynamic> json) => Transaction( factory Transaction.fromJson(Map<String, dynamic> json) => Transaction(
id: json['id'] as String, id: json['id'] as String,
amount: (json['amount'] as num).toDouble(), amount: (json['amount'] as num).toDouble(),
category: json['category'] as String, category: json['category'] as String,
type: TransactionType.values.firstWhere( type: TransactionType.values.firstWhere((e) => e.name == json['type']),
(e) => e.name == json['type'], date: DateTime.parse(json['date'] as String),
), note: json['note'] as String?,
date: DateTime.parse(json['date'] as String), recurrence: json['recurrence'] != null
note: json['note'] as String?, ? RecurrenceType.values.firstWhere(
recurrence: json['recurrence'] != null (e) => e.name == json['recurrence'],
? RecurrenceType.values.firstWhere( orElse: () => RecurrenceType.none,
(e) => e.name == json['recurrence'], )
orElse: () => RecurrenceType.none, : RecurrenceType.none,
) lastOccurrence: json['lastOccurrence'] != null
: RecurrenceType.none, ? DateTime.parse(json['lastOccurrence'] as String)
lastOccurrence: json['lastOccurrence'] != null : null,
? DateTime.parse(json['lastOccurrence'] as String) currency: json['currency'] as String? ?? '\$',
: null, currencyCode: json['currencyCode'] as String? ?? 'USD',
currency: json['currency'] as String? ?? '\$', accountId: json['accountId'] as int,
currencyCode: json['currencyCode'] as String? ?? 'USD', );
accountId: json['accountId'] as int,
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'amount': amount, 'amount': amount,
'category': category, 'category': category,
'type': type.name, 'type': type.name,
'date': date.toIso8601String(), 'date': date.toIso8601String(),
'note': note, 'note': note,
'recurrence': recurrence.name, 'recurrence': recurrence.name,
'lastOccurrence': lastOccurrence?.toIso8601String(), 'lastOccurrence': lastOccurrence?.toIso8601String(),
'currency': currency, 'currency': currency,
'currencyCode': currencyCode, 'currencyCode': currencyCode,
'accountId': accountId, 'accountId': accountId,
}; };
Transaction copyWith({ Transaction copyWith({
String? id, String? id,
@@ -81,18 +79,17 @@ class Transaction {
String? currency, String? currency,
String? currencyCode, String? currencyCode,
int? accountId, int? accountId,
}) => }) => Transaction(
Transaction( id: id ?? this.id,
id: id ?? this.id, amount: amount ?? this.amount,
amount: amount ?? this.amount, category: category ?? this.category,
category: category ?? this.category, type: type ?? this.type,
type: type ?? this.type, date: date ?? this.date,
date: date ?? this.date, note: note ?? this.note,
note: note ?? this.note, recurrence: recurrence ?? this.recurrence,
recurrence: recurrence ?? this.recurrence, lastOccurrence: lastOccurrence ?? this.lastOccurrence,
lastOccurrence: lastOccurrence ?? this.lastOccurrence, currency: currency ?? this.currency,
currency: currency ?? this.currency, currencyCode: currencyCode ?? this.currencyCode,
currencyCode: currencyCode ?? this.currencyCode, accountId: accountId ?? this.accountId,
accountId: accountId ?? this.accountId, );
);
} }