mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
update
This commit is contained in:
@@ -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';
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user