fixes before build 3

This commit is contained in:
gitea
2026-08-31 17:35:46 +02:00
parent b0708101b0
commit edbfade477
7 changed files with 330 additions and 32 deletions
+150 -17
View File
@@ -2,6 +2,7 @@
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/line_item.dart';
import '../utils/money.dart';
@@ -26,47 +27,147 @@ class ItemEditSheet extends StatefulWidget {
class _ItemEditSheetState extends State<ItemEditSheet> {
late final TextEditingController _nameController;
late final TextEditingController _priceController;
late final TextEditingController _quantityController;
late final TextEditingController _unitPriceController;
late final TextEditingController _totalController;
String? _error;
bool _syncing = false;
@override
void initState() {
super.initState();
final item = widget.item;
final quantity = item?.quantity ?? 1;
final total = item?.price;
final unit = item?.unitPrice ??
(total != null && quantity > 0 ? roundMoney(total / quantity) : null);
_nameController = TextEditingController(text: item?.name ?? '');
_priceController = TextEditingController(
text: item == null ? '' : item.price.toStringAsFixed(2),
_quantityController = TextEditingController(text: '$quantity');
_unitPriceController = TextEditingController(
text: unit == null ? '' : unit.toStringAsFixed(2),
);
_totalController = TextEditingController(
text: total == null ? '' : total.toStringAsFixed(2),
);
_quantityController.addListener(_onQuantityChanged);
_unitPriceController.addListener(_onUnitPriceChanged);
_totalController.addListener(_onTotalChanged);
}
@override
void dispose() {
_quantityController.removeListener(_onQuantityChanged);
_unitPriceController.removeListener(_onUnitPriceChanged);
_totalController.removeListener(_onTotalChanged);
_nameController.dispose();
_priceController.dispose();
_quantityController.dispose();
_unitPriceController.dispose();
_totalController.dispose();
super.dispose();
}
int? get _quantity {
final raw = _quantityController.text.trim();
if (raw.isEmpty) return null;
return int.tryParse(raw);
}
void _setText(TextEditingController controller, String value) {
if (controller.text == value) return;
controller.value = TextEditingValue(
text: value,
selection: TextSelection.collapsed(offset: value.length),
);
}
void _onQuantityChanged() {
if (_syncing) return;
final quantity = _quantity;
if (quantity == null || quantity <= 0) {
setState(() {});
return;
}
final unit = tryParsePrice(_unitPriceController.text);
final total = tryParsePrice(_totalController.text);
_syncing = true;
if (unit != null) {
_setText(_totalController, roundMoney(quantity * unit).toStringAsFixed(2));
} else if (total != null) {
_setText(
_unitPriceController,
roundMoney(total / quantity).toStringAsFixed(2),
);
}
_syncing = false;
setState(() {});
}
void _onUnitPriceChanged() {
if (_syncing) return;
final quantity = _quantity;
final unit = tryParsePrice(_unitPriceController.text);
if (quantity == null || quantity <= 0 || unit == null) {
setState(() {});
return;
}
_syncing = true;
_setText(_totalController, roundMoney(quantity * unit).toStringAsFixed(2));
_syncing = false;
setState(() {});
}
void _onTotalChanged() {
if (_syncing) return;
final quantity = _quantity;
final total = tryParsePrice(_totalController.text);
if (quantity == null || quantity <= 0 || total == null) {
setState(() {});
return;
}
_syncing = true;
_setText(
_unitPriceController,
roundMoney(total / quantity).toStringAsFixed(2),
);
_syncing = false;
setState(() {});
}
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.');
final quantity = _quantity;
final unit = tryParsePrice(_unitPriceController.text);
final total = tryParsePrice(_totalController.text);
if (name.isEmpty) {
setState(() => _error = 'Enter a product name.');
return;
}
final rounded = roundMoney(price);
final quantity = widget.item?.quantity;
if (quantity == null || quantity <= 0) {
setState(() => _error = 'Enter a quantity of at least 1.');
return;
}
if (total == null && unit == null) {
setState(() => _error = 'Enter a unit price or a total.');
return;
}
final resolvedTotal = total ?? roundMoney(quantity * unit!);
final resolvedUnit = unit ?? roundMoney(resolvedTotal / quantity);
final hasMulti = quantity > 1;
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,
discount: widget.item?.price == rounded
price: roundMoney(resolvedTotal),
quantity: hasMulti ? quantity : null,
unitPrice: hasMulti ? roundMoney(resolvedUnit) : null,
discount: widget.item?.price == roundMoney(resolvedTotal)
? widget.item?.discount ?? 0
: 0,
),
@@ -76,6 +177,11 @@ class _ItemEditSheetState extends State<ItemEditSheet> {
@override
Widget build(BuildContext context) {
final bottom = MediaQuery.viewInsetsOf(context).bottom;
final quantity = _quantity;
final unit = tryParsePrice(_unitPriceController.text);
final preview = quantity != null && quantity > 1 && unit != null
? '$quantity × ${formatEuro(unit)}'
: null;
return Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
@@ -95,10 +201,37 @@ class _ItemEditSheetState extends State<ItemEditSheet> {
autofocus: widget.item == null,
),
const SizedBox(height: 12),
Row(
children: [
SizedBox(
width: 88,
child: TextField(
controller: _quantityController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(labelText: 'Qty'),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _unitPriceController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
decoration: const InputDecoration(labelText: 'Unit price'),
),
),
],
),
const SizedBox(height: 12),
TextField(
controller: _priceController,
controller: _totalController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(labelText: 'Price'),
decoration: InputDecoration(
labelText: 'Total price',
helperText: preview,
),
),
if (_error != null) ...[
const SizedBox(height: 8),