/// 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 createState() => _TransactionDetailScreenState(); } class _TransactionDetailScreenState extends State { late final TextEditingController _notesController; var _dirty = false; @override void initState() { super.initState(); final notes = context.read().metaFor(widget.tx).notes; _notesController = TextEditingController(text: notes); } @override void dispose() { _notesController.dispose(); super.dispose(); } Future _saveNotes() async { await context.read().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().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().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().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, ), ], ), ), ); } }