/// Home: balance overview; tap balance to open transactions. library; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/bank_provider.dart'; import '../providers/tx_meta_provider.dart'; import '../utils/app_theme.dart'; import '../utils/connect_bank.dart'; import '../utils/helpers.dart'; import '../utils/navigation.dart'; import '../widgets/empty_state.dart'; import '../widgets/loading_view.dart'; import '../widgets/responsive_body.dart'; import '../widgets/skip_build_when_covered.dart'; import 'transaction_detail_screen.dart'; import 'transactions_screen.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { final bank = context.watch(); return Scaffold( appBar: AppBar( title: const Text('Home'), actions: bankAppBarActions( context: context, onRefresh: bank.refresh, refreshing: bank.isLoading, ), ), body: RefreshIndicator( onRefresh: bank.refresh, child: ResponsiveBody( child: _HomeBody(bank: bank), ), ), ); } } class _HomeBody extends StatelessWidget { const _HomeBody({required this.bank}); final BankController bank; void _openTransactions(BuildContext context) { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const TransactionsScreen()), ); } @override Widget build(BuildContext context) { // Stay subscribed, but skip heavy scans while covered by another route // (e.g. transaction detail applying labels to many similar txs). context.watch(); if (bank.isLoading && bank.balance == null && !bank.hasData) { return const LoadingView(message: 'Checking bank link…'); } if (!bank.isLinked && !bank.hasData) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ EmptyState( icon: Icons.account_balance_outlined, title: 'Bank not linked', message: bank.error ?? 'Connect your bank to see balance and categorize spending.', actionLabel: bank.isConnecting ? 'Opening…' : 'Connect bank', onAction: () => runConnectBank(context, bank), ), ], ); } if (bank.error != null && bank.balance == null && !bank.hasData) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ EmptyState( icon: Icons.error_outline, title: 'Could not load balance', message: bank.error!, actionLabel: 'Retry', onAction: bank.refresh, ), ], ); } return SkipBuildWhenCovered( builder: (context) { final balance = bank.balance; final theme = Theme.of(context); final colors = theme.colorScheme; final meta = context.watch(); final recent = bank.transactions.take(5).toList(growable: false); final categorized = meta.labeledTransactionCount(bank.transactions); return ListView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 8, bottom: 32), children: [ Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () => _openTransactions(context), child: Ink( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( colors: [colors.primary, colors.tertiary], ), boxShadow: [ BoxShadow( color: colors.primary.withValues(alpha: 0.28), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Current balance', style: theme.textTheme.labelMedium?.copyWith( color: colors.onPrimary.withValues(alpha: 0.8), ), ), const SizedBox(height: 8), Text( balance == null ? '—' : formatMoney(balance.amount, balance.currency), style: theme.textTheme.headlineMedium?.copyWith( color: colors.onPrimary, ), ), const SizedBox(height: 12), Row( children: [ Icon( Icons.receipt_long, size: 16, color: colors.onPrimary.withValues(alpha: 0.9), ), const SizedBox(width: 6), Expanded( child: Text( 'Tap to view transactions', style: theme.textTheme.labelMedium?.copyWith( color: colors.onPrimary.withValues(alpha: 0.9), ), ), ), Icon( Icons.chevron_right, color: colors.onPrimary.withValues(alpha: 0.9), ), ], ), ], ), ), ), ), const SizedBox(height: 20), Row( children: [ Expanded( child: _StatChip( label: 'Transactions', value: '${bank.transactions.length}', icon: Icons.list_alt, ), ), const SizedBox(width: 10), Expanded( child: _StatChip( label: 'Categorized', value: '$categorized', icon: Icons.label_outline, ), ), ], ), if (recent.isNotEmpty) ...[ const SizedBox(height: 24), Row( children: [ Text('Recent', style: theme.textTheme.titleMedium), const Spacer(), TextButton( onPressed: () => _openTransactions(context), child: const Text('See all'), ), ], ), const SizedBox(height: 4), for (final tx in recent) ListTile( contentPadding: EdgeInsets.zero, dense: true, leading: Builder( builder: (context) { final labels = meta.leafLabelsFor(meta.metaFor(tx)); final label = labels.isEmpty ? null : labels.first; return Icon( label?.icon ?? Icons.receipt_long_outlined, color: label?.color, ); }, ), title: Text( tx.description, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text(formatIsoDate(tx.date)), trailing: Text( formatMoney(tx.amount, tx.currency), style: theme.textTheme.titleSmall?.copyWith( color: tx.amount >= 0 ? AppTheme.accent : AppTheme.expense, ), ), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => TransactionDetailScreen(tx: tx), ), ); }, ), ], if (bank.error != null) ...[ const SizedBox(height: 12), Text( bank.error!, style: theme.textTheme.bodyMedium?.copyWith(color: colors.error), ), ], ], ); }, ); } } class _StatChip extends StatelessWidget { const _StatChip({ required this.label, required this.value, required this.icon, }); final String label; final String value; final IconData icon; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: colors.surfaceContainerLow, borderRadius: BorderRadius.circular(14), ), child: Row( children: [ Icon(icon, size: 18, color: colors.primary), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(value, style: theme.textTheme.titleMedium), Text(label, style: theme.textTheme.labelMedium), ], ), ), ], ), ); } }