user admin capability
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
/// Admin-only dialog to create 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<BankTransaction?> showAdminCreateTransactionDialog(
|
||||
BuildContext context,
|
||||
) {
|
||||
return showDialog<BankTransaction>(
|
||||
context: context,
|
||||
builder: (context) => const _AdminCreateTransactionDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
class _AdminCreateTransactionDialog extends StatefulWidget {
|
||||
const _AdminCreateTransactionDialog();
|
||||
|
||||
@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;
|
||||
var _flow = TxFlowFilter.expense;
|
||||
var _pending = false;
|
||||
var _saving = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
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');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_description.dispose();
|
||||
_amount.dispose();
|
||||
_currency.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<BankController>();
|
||||
final 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: const Text('Create test transaction'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'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<TxFlowFilter>(
|
||||
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),
|
||||
)
|
||||
: const Text('Create'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -498,24 +498,40 @@ class _BudgetBody extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
HomeWidgetChartStyle.pie || HomeWidgetChartStyle.ring => Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
capLine,
|
||||
const SizedBox(height: 8),
|
||||
spentLine,
|
||||
],
|
||||
),
|
||||
HomeWidgetChartStyle.pie || HomeWidgetChartStyle.ring => half
|
||||
// Stack on half cards so amounts never get crushed beside the ring
|
||||
// (and avoid LayoutBuilder — half rows use IntrinsicHeight).
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
capLine,
|
||||
const SizedBox(height: 6),
|
||||
spentLine,
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ringChart(size: 56),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
capLine,
|
||||
const SizedBox(height: 6),
|
||||
spentLine,
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ringChart(size: 68),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ringChart(size: 68),
|
||||
],
|
||||
),
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -590,8 +606,9 @@ class _BudgetLegendLine extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
// Stretch to the parent width so Expanded+ellipsis can shrink safely
|
||||
// (half-width ring rows can be very tight).
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 7,
|
||||
@@ -601,11 +618,12 @@ class _BudgetLegendLine extends StatelessWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: textColor,
|
||||
|
||||
Reference in New Issue
Block a user