parser logic added

This commit is contained in:
davmar
2026-08-30 12:24:29 +02:00
parent 35d5c90d81
commit 3b698a3667
28 changed files with 3018 additions and 346 deletions
+32
View File
@@ -0,0 +1,32 @@
/// Confirm a destructive action before it runs.
library;
import 'package:flutter/material.dart';
Future<bool> confirmAction(
BuildContext context, {
required String title,
required String message,
String confirmLabel = 'Delete',
}) async {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(confirmLabel),
),
],
);
},
);
return result == true;
}
+113
View File
@@ -0,0 +1,113 @@
/// Bottom sheet to add or edit a priced line item.
library;
import 'package:flutter/material.dart';
import '../models/line_item.dart';
import '../utils/money.dart';
Future<LineItem?> showItemEditSheet(BuildContext context, {LineItem? item}) {
return showModalBottomSheet<LineItem>(
context: context,
isScrollControlled: true,
showDragHandle: true,
builder: (context) => ItemEditSheet(item: item),
);
}
class ItemEditSheet extends StatefulWidget {
const ItemEditSheet({super.key, this.item});
final LineItem? item;
@override
State<ItemEditSheet> createState() => _ItemEditSheetState();
}
class _ItemEditSheetState extends State<ItemEditSheet> {
late final TextEditingController _nameController;
late final TextEditingController _priceController;
String? _error;
@override
void initState() {
super.initState();
final item = widget.item;
_nameController = TextEditingController(text: item?.name ?? '');
_priceController = TextEditingController(
text: item == null ? '' : item.price.toStringAsFixed(2),
);
}
@override
void dispose() {
_nameController.dispose();
_priceController.dispose();
super.dispose();
}
void _save() {
final name = _nameController.text.trim();
final price = tryParsePrice(_priceController.text);
if (name.isEmpty || price == null) {
setState(() => _error = 'Enter a product name and a price.');
return;
}
final rounded = roundMoney(price);
final quantity = widget.item?.quantity;
Navigator.of(context).pop(
LineItem(
id:
widget.item?.id ??
'manual-${DateTime.now().microsecondsSinceEpoch}',
name: name,
price: rounded,
quantity: quantity,
unitPrice: quantity != null && quantity > 0
? roundMoney(rounded / quantity)
: widget.item?.unitPrice,
),
);
}
@override
Widget build(BuildContext context) {
final bottom = MediaQuery.viewInsetsOf(context).bottom;
return Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
widget.item == null ? 'Add item' : 'Edit item',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
TextField(
controller: _nameController,
textCapitalization: TextCapitalization.sentences,
decoration: const InputDecoration(labelText: 'Product name'),
autofocus: widget.item == null,
),
const SizedBox(height: 12),
TextField(
controller: _priceController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(labelText: 'Price'),
),
if (_error != null) ...[
const SizedBox(height: 8),
Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
const SizedBox(height: 16),
FilledButton(onPressed: _save, child: const Text('Save')),
],
),
);
}
}
+101
View File
@@ -0,0 +1,101 @@
/// Name prompt that owns its text controller for the dialog lifetime.
library;
import 'package:flutter/material.dart';
Future<String?> showNameDialog(
BuildContext context, {
required String title,
String? initial,
String label = 'Name',
String confirmLabel = 'Save',
}) {
return showDialog<String>(
context: context,
builder: (context) => _NameDialog(
title: title,
initial: initial ?? '',
label: label,
confirmLabel: confirmLabel,
),
);
}
class _NameDialog extends StatefulWidget {
const _NameDialog({
required this.title,
required this.initial,
required this.label,
required this.confirmLabel,
});
final String title;
final String initial;
final String label;
final String confirmLabel;
@override
State<_NameDialog> createState() => _NameDialogState();
}
class _NameDialogState extends State<_NameDialog> {
late final TextEditingController _controller;
String? _error;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initial);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _save() {
final name = _controller.text.trim();
if (name.isEmpty) {
setState(() => _error = 'Enter a name.');
return;
}
Navigator.of(context).pop(name);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _controller,
autofocus: true,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(labelText: widget.label),
onSubmitted: (_) => _save(),
),
if (_error != null) ...[
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: Text(
_error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
TextButton(onPressed: _save, child: Text(widget.confirmLabel)),
],
);
}
}