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
@@ -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_riverpod/flutter_riverpod.dart';
import '../../../core/constants.dart';
import '../../../shared/models/transaction.dart';
import '../../dashboard/provider.dart';
class TypeToggle extends StatelessWidget {
class TypeToggle extends ConsumerWidget {
final TransactionType selected;
final ValueChanged<TransactionType> onChanged;
final bool isDark;
@@ -15,7 +17,11 @@ class TypeToggle extends StatelessWidget {
});
@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(
height: 56,
decoration: BoxDecoration(
@@ -28,53 +34,36 @@ class TypeToggle extends StatelessWidget {
child: Row(
children: [
Expanded(
child: GestureDetector(
child: _TypeOption(
icon: Icons.arrow_downward_rounded,
label: 'Income',
color: AppColors.income,
isSelected: selected == TransactionType.income,
onTap: () => onChanged(TransactionType.income),
child: AnimatedContainer(
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,
),
),
),
isDark: isDark,
),
),
Expanded(
child: GestureDetector(
child: _TypeOption(
icon: Icons.arrow_upward_rounded,
label: 'Expense',
color: AppColors.expense,
isSelected: selected == TransactionType.expense,
onTap: () => onChanged(TransactionType.expense),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: selected == TransactionType.expense
? AppColors.expense.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(11),
),
child: Center(
child: Icon(
Icons.arrow_upward_rounded,
color: selected == TransactionType.expense
? AppColors.expense
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.4),
size: 20,
),
),
),
isDark: isDark,
),
),
Expanded(
child: _TypeOption(
icon: Icons.swap_horiz_rounded,
label: 'Transfer',
color: Colors.blueAccent,
isSelected: selected == TransactionType.transfer,
onTap: transferDisabled
? null
: () => onChanged(TransactionType.transfer),
isDark: isDark,
disabled: transferDisabled,
),
),
],
@@ -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,
),
),
),
],
),
),
),
);
}
}