import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../core/constants.dart'; import '../../../core/l10n/app_strings.dart'; import '../../../core/l10n/locale_provider.dart'; import '../../../shared/models/transaction.dart'; import '../../../shared/models/account.dart'; import '../../../shared/providers/amount_format_provider.dart'; import '../../../shared/utils/currency_utils.dart'; import '../../../shared/widgets/byn_sign.dart'; import '../../settings/provider.dart'; import '../provider.dart'; class TransactionTile extends ConsumerWidget { final Transaction transaction; const TransactionTile({super.key, required this.transaction}); Border? _themeBorder(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Border.all( color: isDark ? Colors.white.withOpacity(0.08) : const Color(0xFFDDDDEE), width: 1, ); } @override Widget build(BuildContext context, WidgetRef ref) { final s = ref.watch(stringsProvider); final fmt = ref.watch(amountFormatProvider); final isTransfer = transaction.category == 'Transfer'; final isIncome = transaction.type == TransactionType.income; final color = isTransfer ? const Color(0xFF7C6DED) : (isIncome ? AppColors.income : AppColors.expense); final catColor = isTransfer ? const Color(0xFF7C6DED) : (AppCategories.colors[transaction.category] ?? AppColors.accent); final catIcon = isTransfer ? Icons.swap_horiz_rounded : (AppCategories.icons[transaction.category] ?? Icons.category_rounded); final activeAccount = ref.watch(activeAccountProvider); final displayCurrency = activeAccount?.currency ?? ref.watch(currencyProvider).code; final showConverted = transaction.currencyCode != displayCurrency; final exchangeService = ref.watch(exchangeRateServiceProvider); final convertedAmount = showConverted ? exchangeService.convert( transaction.amount, transaction.currencyCode, displayCurrency, ) : 0.0; final displaySymbol = currencyMap[displayCurrency]?.symbol ?? ''; final accounts = ref.watch(accountsProvider).valueOrNull ?? []; final txAccount = accounts.firstWhereOrNull( (a) => a.id == transaction.accountId, ); String accountLabel = txAccount?.name ?? ''; if (accountLabel.length > 10) { accountLabel = '${accountLabel.substring(0, 10)}...'; } final pairs = ref.watch(transferPairsProvider); final counterpart = pairs[transaction.id]; return GestureDetector( onTap: () => context.push('/add', extra: transaction), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(16), border: _themeBorder(context), ), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: catColor.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), child: Icon(catIcon, color: catColor, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: isTransfer ? _buildTransferLabel( context, ref, counterpart, accounts, activeAccount, ) : Text( s.categoryLabel(transaction.category), style: Theme.of(context).textTheme.bodyMedium ?.copyWith( fontWeight: FontWeight.w600, color: Theme.of( context, ).colorScheme.onSurface, ), overflow: TextOverflow.ellipsis, ), ), if (!isTransfer && activeAccount == null && accountLabel.isNotEmpty) ...[ const SizedBox(width: 6), _AccountTag(label: accountLabel), ], ], ), if (transaction.note != null && transaction.note!.isNotEmpty) Text( transaction.note!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withOpacity(0.6), ), overflow: TextOverflow.ellipsis, ) else Text( DateFormat( 'd MMM yyyy ยท HH:mm', s.dateLocale, ).format(transaction.date), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurface.withOpacity(0.6), ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ transaction.currencyCode == 'BYN' ? Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( _getAmountPrefix( isTransfer, isIncome, activeAccount, ), style: Theme.of(context).textTheme.bodyMedium ?.copyWith( color: _getAmountColor( context, isTransfer, isIncome, activeAccount, color, ), fontWeight: FontWeight.w700, ), ), BynSign( fontSize: 14, color: _getAmountColor( context, isTransfer, isIncome, activeAccount, color, ), ), const SizedBox(width: 2), Text( formatAmount('', transaction.amount, fmt), style: Theme.of(context).textTheme.bodyMedium ?.copyWith( color: _getAmountColor( context, isTransfer, isIncome, activeAccount, color, ), fontWeight: FontWeight.w700, ), ), ], ) : Text( '${_getAmountPrefix(isTransfer, isIncome, activeAccount)}${formatAmount(transaction.currency, transaction.amount, fmt)}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: _getAmountColor( context, isTransfer, isIncome, activeAccount, color, ), fontWeight: FontWeight.w700, ), ), if (showConverted) ...[ if (displayCurrency == 'BYN') Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( 'โ‰ˆ ', style: TextStyle( fontSize: 11, color: color.withOpacity(0.5), fontWeight: FontWeight.w400, height: 1.3, ), ), BynSign(fontSize: 11, color: color.withOpacity(0.5)), const SizedBox(width: 2), Text( formatAmount('', convertedAmount, fmt), style: TextStyle( fontSize: 11, color: color.withOpacity(0.5), fontWeight: FontWeight.w400, height: 1.3, ), ), ], ) else Text( 'โ‰ˆ ${formatAmount(displaySymbol, convertedAmount, fmt)}', style: TextStyle( fontSize: 11, color: color.withOpacity(0.5), fontWeight: FontWeight.w400, height: 1.3, ), ), ], ], ), ], ), ), ); } Widget _buildTransferLabel( BuildContext context, WidgetRef ref, Transaction? counterpart, List accounts, Account? activeAccount, ) { final s = ref.watch(stringsProvider); if (counterpart == null) { return Text( s.transferLabel, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurface, ), overflow: TextOverflow.ellipsis, ); } final isExpense = transaction.type == TransactionType.expense; final sourceAccountId = isExpense ? transaction.accountId : counterpart.accountId; final destAccountId = isExpense ? counterpart.accountId : transaction.accountId; final sourceAccountName = _accountName(accounts, sourceAccountId); final destAccountName = _accountName(accounts, destAccountId); final onSurface = Theme.of(context).colorScheme.onSurface; if (activeAccount == null) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text( s.transferLabel, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: onSurface, ), ), const SizedBox(width: 6), Flexible( child: _TransferChip( label: '$sourceAccountName โ†’ $destAccountName', icon: Icons.swap_horiz_rounded, ), ), ], ); } if (isExpense) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text( s.transferLabel, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: onSurface, ), ), const SizedBox(width: 6), Text( s.transferTo, style: TextStyle( fontSize: 11, color: onSurface.withOpacity(0.45), fontWeight: FontWeight.w400, ), ), const SizedBox(width: 4), Flexible(child: _TransferChip(label: destAccountName, icon: null)), ], ); } else { return Row( mainAxisSize: MainAxisSize.min, children: [ Text( s.transferLabel, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: onSurface, ), ), const SizedBox(width: 6), Text( s.transferFrom, style: TextStyle( fontSize: 11, color: onSurface.withOpacity(0.45), fontWeight: FontWeight.w400, ), ), const SizedBox(width: 4), Flexible(child: _TransferChip(label: sourceAccountName, icon: null)), ], ); } } String _accountName(List accounts, int? id) { if (id == null) return '?'; return accounts.firstWhereOrNull((a) => a.id == id)?.name ?? '?'; } String _getAmountPrefix( bool isTransfer, bool isIncome, Account? activeAccount, ) { if (isTransfer && activeAccount == null && !isIncome) { return ''; } return isIncome ? '+ ' : '\u2212 '; } Color _getAmountColor( BuildContext context, bool isTransfer, bool isIncome, Account? activeAccount, Color defaultColor, ) { if (isTransfer && activeAccount == null && !isIncome) { return Theme.of(context).colorScheme.onSurface.withOpacity(0.8); } if (isTransfer) { return isIncome ? AppColors.income : AppColors.expense; } return defaultColor; } } class _AccountTag extends StatelessWidget { final String label; const _AccountTag({required this.label}); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return IntrinsicWidth( child: Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: isDark ? Colors.white.withOpacity(0.08) : const Color(0xFFF0EFFE), borderRadius: BorderRadius.circular(6), border: Border.all( color: isDark ? Colors.white.withOpacity(0.12) : const Color(0xFFD0CAFF), width: 1, ), ), child: Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: isDark ? Colors.white.withOpacity(0.55) : const Color(0xFF7C6DED), letterSpacing: 0.1, ), ), ), ); } } class _TransferChip extends StatelessWidget { final String label; final IconData? icon; const _TransferChip({required this.label, this.icon}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFF7C6DED).withOpacity(0.12), borderRadius: BorderRadius.circular(20), border: Border.all( color: const Color(0xFF7C6DED).withOpacity(0.35), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 11, color: const Color(0xFF7C6DED)), const SizedBox(width: 4), ], Flexible( child: Text( label, style: const TextStyle( fontSize: 11, color: Color(0xFF7C6DED), fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ], ), ); } } class EmptyState extends StatelessWidget { final AppStrings strings; const EmptyState({super.key, required this.strings}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, shape: BoxShape.circle, ), child: Icon( Icons.receipt_long_rounded, size: 48, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), ), const SizedBox(height: 16), Text( strings.noTransactions, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), Text( strings.addFirstTx, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), ), ], ), ); } }