/// Admin-only dialog to create or edit a local test bank transaction. library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../models/bank.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; import '../utils/helpers.dart'; Future showAdminCreateTransactionDialog( BuildContext context, { BankTransaction? existing, }) { return showDialog( context: context, builder: (context) => _AdminCreateTransactionDialog(existing: existing), ); } String _stripManualStamp(String description) => description.replaceFirst(RegExp(r' ยท#\d+$'), ''); class _AdminCreateTransactionDialog extends StatefulWidget { const _AdminCreateTransactionDialog({this.existing}); final BankTransaction? existing; @override State<_AdminCreateTransactionDialog> createState() => _AdminCreateTransactionDialogState(); } class _AdminCreateTransactionDialogState extends State<_AdminCreateTransactionDialog> { late DateTime _date; late final TextEditingController _description; late final TextEditingController _amount; late final TextEditingController _currency; late TxFlowFilter _flow; late bool _pending; var _saving = false; String? _error; bool get _editing => widget.existing != null; @override void initState() { super.initState(); final existing = widget.existing; if (existing != null) { final parsed = DateTime.tryParse(existing.date) ?? DateTime.now(); _date = DateTime(parsed.year, parsed.month, parsed.day); _description = TextEditingController( text: _stripManualStamp(existing.description), ); _amount = TextEditingController( text: existing.amount.abs().toStringAsFixed(2), ); _currency = TextEditingController(text: existing.currency); _flow = existing.amount >= 0 ? TxFlowFilter.income : TxFlowFilter.expense; _pending = existing.pending; } else { final now = DateTime.now(); _date = DateTime(now.year, now.month, now.day); _description = TextEditingController(text: 'Test merchant'); _amount = TextEditingController(text: '12.50'); _currency = TextEditingController(text: 'EUR'); _flow = TxFlowFilter.expense; _pending = false; } } @override void dispose() { _description.dispose(); _amount.dispose(); _currency.dispose(); super.dispose(); } Future _pickDate() async { final picked = await showDatePicker( context: context, initialDate: _date, firstDate: DateTime(2015), lastDate: DateTime.now().add(const Duration(days: 365)), ); if (picked == null || !mounted) return; setState(() => _date = DateTime(picked.year, picked.month, picked.day)); } Future _save() async { final raw = _amount.text.trim().replaceAll(',', '.'); final parsed = double.tryParse(raw); if (parsed == null || parsed <= 0) { setState(() => _error = 'Enter a positive amount'); return; } final signed = _flow == TxFlowFilter.income ? parsed : -parsed; setState(() { _saving = true; _error = null; }); try { final bank = context.read(); final existing = widget.existing; final BankTransaction tx; if (existing != null) { tx = await bank.updateManualTransaction( previous: existing, date: _date, description: _description.text, amount: signed, currency: _currency.text, pending: _pending, ); } else { tx = await bank.addManualTransaction( date: _date, description: _description.text, amount: signed, currency: _currency.text, pending: _pending, ); } if (!mounted) return; Navigator.pop(context, tx); } catch (error) { if (!mounted) return; setState(() { _saving = false; _error = '$error'; }); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return AlertDialog( title: Text(_editing ? 'Edit test transaction' : 'Create test transaction'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( _editing ? 'Updates this device and removes the old row on the next PocketBase sync.' : 'Saved on this device and included in the next PocketBase sync.', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, title: const Text('Date'), subtitle: Text(formatDate(_date)), trailing: const Icon(Icons.calendar_today_outlined, size: 20), onTap: _saving ? null : _pickDate, ), TextField( controller: _description, enabled: !_saving, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( labelText: 'Description', ), ), const SizedBox(height: 12), SegmentedButton( segments: const [ ButtonSegment( value: TxFlowFilter.expense, label: Text('Expense'), icon: Icon(Icons.north_east, size: 16), ), ButtonSegment( value: TxFlowFilter.income, label: Text('Income'), icon: Icon(Icons.south_west, size: 16), ), ], selected: {_flow}, onSelectionChanged: _saving ? null : (selection) { setState(() => _flow = selection.first); }, ), const SizedBox(height: 12), Row( children: [ Expanded( flex: 2, child: TextField( controller: _amount, enabled: !_saving, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]')), ], decoration: const InputDecoration( labelText: 'Amount', ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _currency, enabled: !_saving, textCapitalization: TextCapitalization.characters, decoration: const InputDecoration( labelText: 'Currency', ), ), ), ], ), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Pending'), value: _pending, onChanged: _saving ? null : (value) => setState(() => _pending = value), ), if (_error != null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( _error!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), ), ), ], ), ), actions: [ TextButton( onPressed: _saving ? null : () => Navigator.pop(context), child: const Text('Cancel'), ), FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(_editing ? 'Save' : 'Create'), ), ], ); } }