ok for now

This commit is contained in:
davmar
2026-09-06 21:58:59 +02:00
parent 773187f0af
commit 87afe271b7
20 changed files with 2271 additions and 377 deletions
+153
View File
@@ -0,0 +1,153 @@
/// Transaction detail: category, recurring, and user notes.
library;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/bank.dart';
import '../models/transaction_meta.dart';
import '../providers/tx_meta_provider.dart';
import '../utils/helpers.dart';
import '../widgets/responsive_body.dart';
class TransactionDetailScreen extends StatefulWidget {
const TransactionDetailScreen({super.key, required this.tx});
final BankTransaction tx;
@override
State<TransactionDetailScreen> createState() =>
_TransactionDetailScreenState();
}
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
late final TextEditingController _notesController;
var _dirty = false;
@override
void initState() {
super.initState();
final notes = context.read<TxMetaController>().metaFor(widget.tx).notes;
_notesController = TextEditingController(text: notes);
}
@override
void dispose() {
_notesController.dispose();
super.dispose();
}
Future<void> _saveNotes() async {
await context.read<TxMetaController>().setNotes(
widget.tx,
_notesController.text,
);
if (!mounted) return;
setState(() => _dirty = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Notes saved')),
);
}
@override
Widget build(BuildContext context) {
final tx = widget.tx;
final meta = context.watch<TxMetaController>().metaFor(tx);
final theme = Theme.of(context);
final colors = theme.colorScheme;
final amountColor = tx.amount >= 0 ? colors.primary : colors.error;
return Scaffold(
appBar: AppBar(
title: const Text('Transaction'),
actions: [
if (_dirty)
TextButton(
onPressed: _saveNotes,
child: const Text('Save'),
),
],
),
body: ResponsiveBody(
child: ListView(
padding: const EdgeInsets.only(top: 8, bottom: 32),
children: [
Text(
formatMoney(tx.amount, tx.currency),
style: theme.textTheme.headlineMedium?.copyWith(color: amountColor),
),
const SizedBox(height: 8),
Text(tx.description, style: theme.textTheme.titleMedium),
const SizedBox(height: 4),
Text(
[
formatIsoDate(tx.date),
if (tx.pending) 'Pending',
].join(' · '),
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 24),
SwitchListTile(
contentPadding: EdgeInsets.zero,
secondary: const Icon(Icons.autorenew),
title: const Text('Recurring'),
subtitle: const Text('Salary, mortgage, subscriptions…'),
value: meta.recurring,
onChanged: (value) {
context.read<TxMetaController>().setRecurring(tx, value);
},
),
const SizedBox(height: 16),
Text('Category', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final category in TxCategory.values)
ChoiceChip(
avatar: Icon(
category.icon,
size: 16,
color: category.color,
),
label: Text(category.label),
selected: meta.category == category,
onSelected: (_) {
context.read<TxMetaController>().setCategory(tx, category);
},
),
],
),
const SizedBox(height: 24),
Text('Notes', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
TextField(
controller: _notesController,
minLines: 3,
maxLines: 6,
textInputAction: TextInputAction.newline,
decoration: const InputDecoration(
hintText: 'Add a private note…',
),
onChanged: (_) {
if (!_dirty) setState(() => _dirty = true);
},
),
const SizedBox(height: 12),
FilledButton(
onPressed: _dirty ? _saveNotes : null,
child: const Text('Save notes'),
),
const SizedBox(height: 16),
Text(
'Matching descriptions reuse this category automatically. '
'Exact same amount + description also copies notes.',
style: theme.textTheme.labelMedium,
),
],
),
),
);
}
}