parser logic added
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user