154 lines
4.8 KiB
Dart
154 lines
4.8 KiB
Dart
/// 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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|