admin user able to create test txs

This commit is contained in:
davmar
2026-09-10 23:18:24 +02:00
parent c4f0ca785e
commit f8e293f823
13 changed files with 507 additions and 111 deletions
+109 -9
View File
@@ -6,10 +6,12 @@ import 'package:provider/provider.dart';
import '../models/bank.dart';
import '../models/transaction_meta.dart';
import '../providers/auth_provider.dart';
import '../providers/bank_provider.dart';
import '../providers/tx_meta_provider.dart';
import '../utils/app_theme.dart';
import '../utils/helpers.dart';
import '../widgets/admin_create_transaction_dialog.dart';
import '../widgets/label_parent_picker.dart';
import '../widgets/recurring_template_sheet.dart';
import '../widgets/responsive_body.dart';
@@ -26,6 +28,7 @@ class TransactionDetailScreen extends StatefulWidget {
}
class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
late BankTransaction _tx;
late final TextEditingController _notesController;
late final TextEditingController _labelSearchController;
late SimilarMatchCriteria _matchCriteria;
@@ -41,11 +44,12 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
@override
void initState() {
super.initState();
_tx = widget.tx;
final meta = context.read<TxMetaController>();
final notes = meta.metaFor(widget.tx).notes;
final notes = meta.metaFor(_tx).notes;
_notesController = TextEditingController(text: notes);
_labelSearchController = TextEditingController();
final tx = widget.tx;
final tx = _tx;
_matchCriteria = SimilarMatchCriteria(
pattern: defaultSimilarPattern(tx.description),
flow: tx.amount < 0
@@ -63,7 +67,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
if (!mounted) return;
final all = context.read<BankController>().transactions;
final count = context.read<TxMetaController>().countSimilar(
widget.tx,
_tx,
all,
criteria: _matchCriteria,
);
@@ -88,14 +92,14 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
List<BankTransaction> _similarScope(TxMetaController meta) {
final all = context.read<BankController>().transactions;
if (!_applyToSimilar) return const [];
return meta.similarTo(widget.tx, all, criteria: _matchCriteria);
return meta.similarTo(_tx, all, criteria: _matchCriteria);
}
Future<void> _editMatchCriteria() async {
final all = context.read<BankController>().transactions;
final result = await showSimilarMatchDialog(
context,
prototype: widget.tx,
prototype: _tx,
all: all,
initial: _matchCriteria,
);
@@ -138,7 +142,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
final similar = _similarScope(meta);
final count = similar.isEmpty ? 1 : similar.length;
await meta.setNotes(
widget.tx,
_tx,
_notesController.text,
applyToSimilar: similar,
matchPattern: _pattern,
@@ -180,7 +184,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
() async {
final meta = context.read<TxMetaController>();
final similar = toMatched ? _similarScope(meta) : const <BankTransaction>[];
final targets = similar.isEmpty ? [widget.tx] : similar;
final targets = similar.isEmpty ? [_tx] : similar;
await meta.setLabelsForMany(
targets,
_draftLabelIds.toList(growable: false),
@@ -188,7 +192,7 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
if (!mounted) return;
setState(() {
_labelsDirty = false;
_draftLabelIds = meta.metaFor(widget.tx).labelIds.toSet();
_draftLabelIds = meta.metaFor(_tx).labelIds.toSet();
});
final count = targets.length;
ScaffoldMessenger.of(context).showSnackBar(
@@ -291,9 +295,79 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
await context.read<TxMetaController>().removeLabel(label.id);
}
Future<void> _editManualTransaction() async {
final updated = await showAdminCreateTransactionDialog(
context,
existing: _tx,
);
if (updated == null || !mounted) return;
final meta = context.read<TxMetaController>();
if (updated.id != _tx.id) {
await meta.rekeyTransaction(_tx.id, updated.id);
}
setState(() {
_tx = updated;
_matchCriteria = SimilarMatchCriteria(
pattern: defaultSimilarPattern(updated.description),
flow: updated.amount < 0
? TxFlowFilter.expense
: updated.amount > 0
? TxFlowFilter.income
: TxFlowFilter.all,
);
_draftLabelIds = meta.metaFor(updated).labelIds.toSet();
_labelsDirty = false;
_similarReady = false;
_similarCount = 0;
});
_refreshSimilarCount();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Test transaction updated')),
);
}
Future<void> _deleteManualTransaction() async {
final ok = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete test transaction?'),
content: const Text(
'This removes the transaction from this device. The remote copy is '
'deleted on the next PocketBase sync.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
);
},
);
if (ok != true || !mounted) return;
final id = _tx.id;
final bank = context.read<BankController>();
final meta = context.read<TxMetaController>();
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
await bank.deleteManualTransaction(_tx);
await meta.clearTransactionMeta(id);
if (!mounted) return;
navigator.pop();
messenger.showSnackBar(
const SnackBar(content: Text('Test transaction deleted')),
);
}
@override
Widget build(BuildContext context) {
final tx = widget.tx;
final tx = _tx;
final metaController = context.watch<TxMetaController>();
final meta = metaController.metaFor(tx);
final similarCount = _similarCount;
@@ -317,11 +391,37 @@ class _TransactionDetailScreenState extends State<TransactionDetailScreen> {
if (keep.contains(entry.label.id)) entry,
];
}();
final canEditManual =
context.watch<AuthController>().isAdmin && tx.isManual;
return Scaffold(
appBar: AppBar(
title: const Text('Transaction'),
actions: [
if (canEditManual)
PopupMenuButton<String>(
enabled: !_busy,
onSelected: (value) {
if (value == 'edit') {
_editManualTransaction();
} else if (value == 'delete') {
_deleteManualTransaction();
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Text('Edit test transaction'),
),
PopupMenuItem(
value: 'delete',
child: Text(
'Delete',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
if (_dirty)
TextButton(
onPressed: _busy ? null : _saveNotes,