114 lines
3.2 KiB
Dart
114 lines
3.2 KiB
Dart
/// 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')),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|