143 lines
3.9 KiB
Dart
143 lines
3.9 KiB
Dart
/// 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<BankController>();
|
|
|
|
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|