user admin capability

This commit is contained in:
davmar
2026-09-10 22:58:52 +02:00
parent 6cc0de8945
commit c4f0ca785e
7 changed files with 420 additions and 21 deletions
@@ -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'),
),
],
);
}
}
+38 -20
View File
@@ -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,