/// Transactions tab: recent bank movements from the ingtrak API. library; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/bank.dart'; import '../providers/bank_provider.dart'; import '../utils/connect_bank.dart'; import '../utils/helpers.dart'; import '../widgets/empty_state.dart'; import '../widgets/loading_view.dart'; import '../widgets/responsive_body.dart'; class TransactionsScreen extends StatelessWidget { const TransactionsScreen({super.key}); @override Widget build(BuildContext context) { final bank = context.watch(); return Scaffold( appBar: AppBar( title: const Text('Transactions'), actions: [ IconButton( tooltip: 'Refresh', onPressed: bank.isLoading ? null : bank.refresh, icon: const Icon(Icons.refresh), ), ], ), body: RefreshIndicator( onRefresh: bank.refresh, child: ResponsiveBody( child: _TransactionsBody(bank: bank), ), ), ); } } class _TransactionsBody extends StatelessWidget { const _TransactionsBody({required this.bank}); final BankController bank; @override Widget build(BuildContext context) { if (bank.isLoading && bank.transactions.isEmpty) { return const LoadingView(message: 'Loading transactions…'); } if (!bank.isLinked) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ EmptyState( icon: Icons.receipt_long_outlined, title: 'No transactions yet', message: bank.error ?? 'Link your bank on the Balance tab to load recent activity.', actionLabel: bank.isConnecting ? 'Opening…' : 'Connect bank', onAction: () => runConnectBank(context, bank), ), ], ); } if (bank.error != null && bank.transactions.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: [ EmptyState( icon: Icons.error_outline, title: 'Could not load transactions', message: bank.error!, actionLabel: 'Retry', onAction: bank.refresh, ), ], ); } if (bank.transactions.isEmpty) { return ListView( physics: const AlwaysScrollableScrollPhysics(), children: const [ EmptyState( icon: Icons.inbox_outlined, title: 'No recent activity', message: 'The backend returned an empty transaction list.', ), ], ); } return ListView.separated( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 8, bottom: 32), itemCount: bank.transactions.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (context, index) { return _TransactionTile(tx: bank.transactions[index]); }, ); } } class _TransactionTile extends StatelessWidget { const _TransactionTile({required this.tx}); final BankTransaction tx; @override Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; final isCredit = tx.amount >= 0; final amountColor = isCredit ? colors.primary : colors.error; return Card( child: ListTile( title: Text( tx.description, maxLines: 2, overflow: TextOverflow.ellipsis, ), subtitle: Text( [ formatIsoDate(tx.date), if (tx.pending) 'Pending', ].where((part) => part.isNotEmpty).join(' · '), ), trailing: Text( formatMoney(tx.amount, tx.currency), style: theme.textTheme.titleMedium?.copyWith(color: amountColor), ), ), ); } }