admin user able to create test txs
This commit is contained in:
@@ -125,36 +125,59 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (auth.isAdmin) ...[
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.add_card_outlined),
|
||||
title: const Text('Create test transaction'),
|
||||
subtitle: const Text(
|
||||
'Admin only · local + syncs to PocketBase',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final tx = await showAdminCreateTransactionDialog(context);
|
||||
if (tx == null || !context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Created “${tx.description}” · '
|
||||
'${tx.amount.toStringAsFixed(2)} ${tx.currency}',
|
||||
),
|
||||
),
|
||||
);
|
||||
// Push so other devices / PB see it soon.
|
||||
final sync = context.read<SyncController>();
|
||||
if (sync.canSync && !sync.isSyncing) {
|
||||
unawaited(sync.syncNow());
|
||||
}
|
||||
},
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.add_card_outlined,
|
||||
color: auth.isAdmin ? null : colors.onSurfaceVariant,
|
||||
),
|
||||
title: const Text('Create test transaction'),
|
||||
subtitle: Text(
|
||||
auth.isAdmin
|
||||
? 'Admin · saved locally and synced to PocketBase'
|
||||
: 'Admin only · enable is_admin on your usersapp user in PocketBase, then sign out and back in',
|
||||
),
|
||||
trailing: auth.isAdmin
|
||||
? const Icon(Icons.chevron_right)
|
||||
: Icon(
|
||||
Icons.lock_outline,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
onTap: () async {
|
||||
if (!auth.isAdmin) {
|
||||
await auth.refreshSession();
|
||||
if (!context.mounted) return;
|
||||
if (!context.read<AuthController>().isAdmin) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Not an admin yet. In PocketBase Admin → '
|
||||
'usersapp → your user → set is_admin = true, '
|
||||
'then sign out and sign in again.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
final tx = await showAdminCreateTransactionDialog(context);
|
||||
if (tx == null || !context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Created “${tx.description}” · '
|
||||
'${tx.amount.toStringAsFixed(2)} ${tx.currency}',
|
||||
),
|
||||
),
|
||||
);
|
||||
final sync = context.read<SyncController>();
|
||||
if (sync.canSync && !sync.isSyncing) {
|
||||
unawaited(sync.syncNow());
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: ListTile(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:provider/provider.dart';
|
||||
import '../models/bank.dart';
|
||||
import '../models/transaction_meta.dart';
|
||||
import '../providers/bank_provider.dart';
|
||||
import '../providers/preferences_provider.dart';
|
||||
import '../providers/tx_meta_provider.dart';
|
||||
import '../utils/app_theme.dart';
|
||||
import '../utils/connect_bank.dart';
|
||||
@@ -17,7 +16,6 @@ import '../utils/helpers.dart';
|
||||
import '../utils/navigation.dart';
|
||||
import '../utils/tx_calendar.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/home_widgets_sheet.dart';
|
||||
import '../widgets/label_filter_dialog.dart';
|
||||
import '../widgets/loading_view.dart';
|
||||
import '../widgets/responsive_body.dart';
|
||||
@@ -277,28 +275,6 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveAsHomeWidget() async {
|
||||
final preferences = context.read<PreferencesController>();
|
||||
final created = await composeHomeFilterWidget(
|
||||
context,
|
||||
initialFilter: TransactionFilterSelection(
|
||||
labelIds: Set<String>.of(_labelFilters),
|
||||
labelMatch: _labelMatch,
|
||||
flow: _flowFilter,
|
||||
amountRange: _amountRange,
|
||||
query: _activeQuery,
|
||||
monthOnly: _searchInMonthOnly,
|
||||
),
|
||||
skipFilterDialog: true,
|
||||
);
|
||||
if (created == null) return;
|
||||
await preferences.addHomeFilterWidget(created);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Added “${created.name}” to Home')),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bank = context.watch<BankController>();
|
||||
@@ -307,11 +283,6 @@ class _TransactionsScreenState extends State<TransactionsScreen> {
|
||||
appBar: AppBar(
|
||||
title: const Text('Transactions'),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Save as Home widget',
|
||||
onPressed: _saveAsHomeWidget,
|
||||
icon: const Icon(Icons.dashboard_customize_outlined),
|
||||
),
|
||||
...bankAppBarActions(
|
||||
context: context,
|
||||
onRefresh: bank.refresh,
|
||||
|
||||
Reference in New Issue
Block a user