app working with sandbox

This commit is contained in:
davmar
2026-09-06 21:13:30 +02:00
parent 8475abeddb
commit 773187f0af
18 changed files with 1909 additions and 100 deletions
+142
View File
@@ -0,0 +1,142 @@
/// 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),
),
),
);
}
}