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