Handle quick changes in line items

This commit is contained in:
Hillel Coren 2021-05-27 20:24:51 +03:00
parent bf569242c6
commit 3d34cfa06a
2 changed files with 341 additions and 269 deletions

View File

@ -264,308 +264,370 @@ class _InvoiceEditItemsDesktopState extends State<InvoiceEditItemsDesktop> {
), ),
), ),
*/ */
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: TypeAheadFormField<String>( skipTraversal: true,
key: ValueKey('__line_item_${index}_name__'), child: Padding(
initialValue: lineItems[index].productKey, padding:
noItemsFoundBuilder: (context) => SizedBox(), const EdgeInsets.only(right: kTableColumnGap),
suggestionsCallback: (pattern) { child: TypeAheadFormField<String>(
return productIds key: ValueKey('__line_item_${index}_name__'),
.where((productId) => productState initialValue: lineItems[index].productKey,
.map[productId].productKey noItemsFoundBuilder: (context) => SizedBox(),
.toLowerCase() suggestionsCallback: (pattern) {
.contains(pattern.toLowerCase())) return productIds
.toList(); .where((productId) => productState
/* .map[productId].productKey
return productIds .toLowerCase()
.where((productId) => productState.map[productId] .contains(pattern.toLowerCase()))
.matchesFilter(pattern)) .toList();
.toList(); /*
*/ return productIds
}, .where((productId) => productState.map[productId]
itemBuilder: (context, productId) { .matchesFilter(pattern))
// TODO fix this .toList();
/* */
return ListTile( },
title: Text(productState.map[suggestion].productKey), itemBuilder: (context, productId) {
); // TODO fix this
*/ /*
return Listener( return ListTile(
child: Container( title: Text(productState.map[suggestion].productKey),
color: Theme.of(context).cardColor, );
child: ListTile( */
title: Text( return Listener(
productState.map[productId].productKey), child: Container(
color: Theme.of(context).cardColor,
child: ListTile(
title: Text(
productState.map[productId].productKey),
),
), ),
), onPointerDown: (_) {
onPointerDown: (_) { if (!kIsWeb) {
if (!kIsWeb) { return;
return; }
}
final item = lineItems[index]; final item = lineItems[index];
final product = productState.map[productId]; final product = productState.map[productId];
final client = final client =
state.clientState.get(invoice.clientId); state.clientState.get(invoice.clientId);
final currency = state final currency = state.staticState
.staticState.currencyMap[client.currencyId]; .currencyMap[client.currencyId];
double cost = product.price; double cost = product.price;
if (company.convertProductExchangeRate && if (company.convertProductExchangeRate &&
invoice.clientId != null && invoice.clientId != null &&
client.currencyId != company.currencyId) { client.currencyId != company.currencyId) {
cost = round(cost * invoice.exchangeRate, cost = round(cost * invoice.exchangeRate,
currency.precision); currency.precision);
} }
final updatedItem = item.rebuild((b) => b final updatedItem = item.rebuild((b) => b
..productKey = product.productKey ..productKey = product.productKey
..notes = ..notes =
item.isTask ? item.notes : product.notes item.isTask ? item.notes : product.notes
..cost = item.isTask && item.cost != 0 ..cost = item.isTask && item.cost != 0
? item.cost ? item.cost
: cost : cost
..quantity = item.isTask || item.quantity != 0 ..quantity =
? item.quantity item.isTask || item.quantity != 0
: viewModel.state.company.defaultQuantity ? item.quantity
? 1 : viewModel.state.company
: product.quantity .defaultQuantity
..customValue1 = product.customValue1 ? 1
..customValue2 = product.customValue2 : product.quantity
..customValue3 = product.customValue3 ..customValue1 = product.customValue1
..customValue4 = product.customValue4 ..customValue2 = product.customValue2
..taxRate1 = product.taxRate1 ..customValue3 = product.customValue3
..taxName1 = product.taxName1 ..customValue4 = product.customValue4
..taxRate2 = product.taxRate2 ..taxRate1 = product.taxRate1
..taxName2 = product.taxName2 ..taxName1 = product.taxName1
..taxRate3 = product.taxRate3 ..taxRate2 = product.taxRate2
..taxName3 = product.taxName3); ..taxName2 = product.taxName2
_onChanged(updatedItem, index, debounce: false); ..taxRate3 = product.taxRate3
_updateTable(); ..taxName3 = product.taxName3);
}, _onChanged(updatedItem, index,
); debounce: false);
}, _updateTable();
onSuggestionSelected: (suggestion) { },
if (kIsWeb) { );
return; },
} onSuggestionSelected: (suggestion) {
if (kIsWeb) {
return;
}
final item = lineItems[index]; final item = lineItems[index];
final product = productState.map[suggestion]; final product = productState.map[suggestion];
final client = final client =
state.clientState.get(invoice.clientId); state.clientState.get(invoice.clientId);
double cost = product.price; double cost = product.price;
if (company.convertProductExchangeRate && if (company.convertProductExchangeRate &&
invoice.clientId != null && invoice.clientId != null &&
client.currencyId != company.currencyId) { client.currencyId != company.currencyId) {
cost = round( cost = round(
cost * invoice.exchangeRate, cost * invoice.exchangeRate,
state state
.staticState .staticState
.currencyMap[client?.currencyId ?? .currencyMap[client?.currencyId ??
company.currencyId] company.currencyId]
.precision); .precision);
} }
final updatedItem = item.rebuild((b) => b final updatedItem = item.rebuild((b) => b
..productKey = product.productKey ..productKey = product.productKey
..notes = item.isTask ? item.notes : product.notes ..notes =
..cost = item.isTask && item.cost != 0 item.isTask ? item.notes : product.notes
? item.cost ..cost = item.isTask && item.cost != 0
: cost ? item.cost
..quantity = item.isTask || item.quantity != 0 : cost
? item.quantity ..quantity = item.isTask || item.quantity != 0
: viewModel.state.company.defaultQuantity ? item.quantity
? 1 : viewModel.state.company.defaultQuantity
: product.quantity ? 1
..customValue1 = product.customValue1 : product.quantity
..customValue2 = product.customValue2 ..customValue1 = product.customValue1
..customValue3 = product.customValue3 ..customValue2 = product.customValue2
..customValue4 = product.customValue4 ..customValue3 = product.customValue3
..taxRate1 = product.taxRate1 ..customValue4 = product.customValue4
..taxName1 = product.taxName1 ..taxRate1 = product.taxRate1
..taxRate2 = product.taxRate2 ..taxName1 = product.taxName1
..taxName2 = product.taxName2 ..taxRate2 = product.taxRate2
..taxRate3 = product.taxRate3 ..taxName2 = product.taxName2
..taxName3 = product.taxName3); ..taxRate3 = product.taxRate3
_onChanged(updatedItem, index, debounce: false); ..taxName3 = product.taxName3);
_updateTable(); _onChanged(updatedItem, index, debounce: false);
}, _updateTable();
textFieldConfiguration: },
TextFieldConfiguration(onChanged: (value) { textFieldConfiguration:
_onChanged( TextFieldConfiguration(onChanged: (value) {
lineItems[index] _onChanged(
.rebuild((b) => b..productKey = value), lineItems[index]
index); .rebuild((b) => b..productKey = value),
}), index);
autoFlipDirection: true, }),
animationStart: 1, autoFlipDirection: true,
debounceDuration: Duration(seconds: 0), animationStart: 1,
)), debounceDuration: Duration(seconds: 0),
Padding( )),
padding: const EdgeInsets.only(right: kTableColumnGap), ),
child: GrowableFormField( Focus(
key: ValueKey('__line_item_${index}_description__'), onFocusChange: (hasFocus) => Debouncer.complete(),
initialValue: lineItems[index].notes, skipTraversal: true,
onChanged: (value) => _onChanged( child: Padding(
lineItems[index].rebuild((b) => b..notes = value), padding: const EdgeInsets.only(right: kTableColumnGap),
index), child: GrowableFormField(
keyboardType: TextInputType.multiline, key: ValueKey('__line_item_${index}_description__'),
initialValue: lineItems[index].notes,
onChanged: (value) => _onChanged(
lineItems[index].rebuild((b) => b..notes = value),
index),
keyboardType: TextInputType.multiline,
),
), ),
), ),
if (company.hasCustomField(customField1)) if (company.hasCustomField(customField1))
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: CustomField( skipTraversal: true,
field: customField1, child: Padding(
value: lineItems[index].customValue1, padding:
hideFieldLabel: true, const EdgeInsets.only(right: kTableColumnGap),
onChanged: (value) => _onChanged( child: CustomField(
lineItems[index] field: customField1,
.rebuild((b) => b..customValue1 = value), value: lineItems[index].customValue1,
index), hideFieldLabel: true,
onSavePressed: widget.entityViewModel.onSavePressed, onChanged: (value) => _onChanged(
lineItems[index]
.rebuild((b) => b..customValue1 = value),
index),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
if (company.hasCustomField(customField2)) if (company.hasCustomField(customField2))
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: CustomField( skipTraversal: true,
field: customField2, child: Padding(
value: lineItems[index].customValue2, padding:
hideFieldLabel: true, const EdgeInsets.only(right: kTableColumnGap),
onChanged: (value) => _onChanged( child: CustomField(
lineItems[index] field: customField2,
.rebuild((b) => b..customValue2 = value), value: lineItems[index].customValue2,
index), hideFieldLabel: true,
onSavePressed: widget.entityViewModel.onSavePressed, onChanged: (value) => _onChanged(
lineItems[index]
.rebuild((b) => b..customValue2 = value),
index),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
if (company.hasCustomField(CustomFieldType.product3)) if (company.hasCustomField(CustomFieldType.product3))
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: CustomField( skipTraversal: true,
field: CustomFieldType.product3, child: Padding(
value: lineItems[index].customValue3, padding:
hideFieldLabel: true, const EdgeInsets.only(right: kTableColumnGap),
onChanged: (value) => _onChanged( child: CustomField(
lineItems[index] field: CustomFieldType.product3,
.rebuild((b) => b..customValue3 = value), value: lineItems[index].customValue3,
index), hideFieldLabel: true,
onSavePressed: widget.entityViewModel.onSavePressed, onChanged: (value) => _onChanged(
lineItems[index]
.rebuild((b) => b..customValue3 = value),
index),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
if (company.hasCustomField(customField4)) if (company.hasCustomField(customField4))
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: CustomField( skipTraversal: true,
field: customField4, child: Padding(
value: lineItems[index].customValue4, padding:
hideFieldLabel: true, const EdgeInsets.only(right: kTableColumnGap),
onChanged: (value) => _onChanged( child: CustomField(
lineItems[index] field: customField4,
.rebuild((b) => b..customValue4 = value), value: lineItems[index].customValue4,
index), hideFieldLabel: true,
onSavePressed: widget.entityViewModel.onSavePressed, onChanged: (value) => _onChanged(
lineItems[index]
.rebuild((b) => b..customValue4 = value),
index),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
if (hasTax1) if (hasTax1)
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: TaxRateDropdown( skipTraversal: true,
onSelected: (taxRate) => _onChanged( child: Padding(
lineItems[index].rebuild((b) => b padding:
..taxName1 = taxRate.name const EdgeInsets.only(right: kTableColumnGap),
..taxRate1 = taxRate.rate), child: TaxRateDropdown(
index), onSelected: (taxRate) => _onChanged(
labelText: null, lineItems[index].rebuild((b) => b
initialTaxName: lineItems[index].taxName1, ..taxName1 = taxRate.name
initialTaxRate: lineItems[index].taxRate1, ..taxRate1 = taxRate.rate),
index),
labelText: null,
initialTaxName: lineItems[index].taxName1,
initialTaxRate: lineItems[index].taxRate1,
),
), ),
), ),
if (hasTax2) if (hasTax2)
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: TaxRateDropdown( skipTraversal: true,
onSelected: (taxRate) => _onChanged( child: Padding(
lineItems[index].rebuild((b) => b padding:
..taxName2 = taxRate.name const EdgeInsets.only(right: kTableColumnGap),
..taxRate2 = taxRate.rate), child: TaxRateDropdown(
index), onSelected: (taxRate) => _onChanged(
labelText: null, lineItems[index].rebuild((b) => b
initialTaxName: lineItems[index].taxName2, ..taxName2 = taxRate.name
initialTaxRate: lineItems[index].taxRate2, ..taxRate2 = taxRate.rate),
index),
labelText: null,
initialTaxName: lineItems[index].taxName2,
initialTaxRate: lineItems[index].taxRate2,
),
), ),
), ),
if (hasTax3) if (hasTax3)
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: TaxRateDropdown( skipTraversal: true,
onSelected: (taxRate) => _onChanged( child: Padding(
lineItems[index].rebuild((b) => b padding:
..taxName3 = taxRate.name const EdgeInsets.only(right: kTableColumnGap),
..taxRate3 = taxRate.rate), child: TaxRateDropdown(
index), onSelected: (taxRate) => _onChanged(
labelText: null, lineItems[index].rebuild((b) => b
initialTaxName: lineItems[index].taxName3, ..taxName3 = taxRate.name
initialTaxRate: lineItems[index].taxRate3, ..taxRate3 = taxRate.rate),
index),
labelText: null,
initialTaxName: lineItems[index].taxName3,
initialTaxRate: lineItems[index].taxRate3,
),
), ),
), ),
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: DecoratedFormField( skipTraversal: true,
key: ValueKey('__line_item_${index}_cost__'), child: Padding(
textAlign: TextAlign.right, padding: const EdgeInsets.only(right: kTableColumnGap),
initialValue: formatNumber( child: DecoratedFormField(
lineItems[index].cost, context, key: ValueKey('__line_item_${index}_cost__'),
formatNumberType: FormatNumberType.inputMoney, textAlign: TextAlign.right,
clientId: invoice.clientId), initialValue: formatNumber(
onChanged: (value) => _onChanged( lineItems[index].cost, context,
lineItems[index] formatNumberType: FormatNumberType.inputMoney,
.rebuild((b) => b..cost = parseDouble(value)), clientId: invoice.clientId),
index), onChanged: (value) => _onChanged(
keyboardType: TextInputType.numberWithOptions( lineItems[index]
decimal: true, signed: true), .rebuild((b) => b..cost = parseDouble(value)),
onSavePressed: widget.entityViewModel.onSavePressed, index),
keyboardType: TextInputType.numberWithOptions(
decimal: true, signed: true),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
if (company.enableProductQuantity || widget.isTasks) if (company.enableProductQuantity || widget.isTasks)
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: DecoratedFormField( skipTraversal: true,
key: ValueKey('__line_item_${index}_quantity__'), child: Padding(
textAlign: TextAlign.right, padding:
initialValue: formatNumber( const EdgeInsets.only(right: kTableColumnGap),
lineItems[index].quantity, context, child: DecoratedFormField(
formatNumberType: FormatNumberType.inputAmount, key: ValueKey('__line_item_${index}_quantity__'),
clientId: invoice.clientId), textAlign: TextAlign.right,
onChanged: (value) => _onChanged( initialValue: formatNumber(
lineItems[index].rebuild( lineItems[index].quantity, context,
(b) => b..quantity = parseDouble(value)), formatNumberType: FormatNumberType.inputAmount,
index), clientId: invoice.clientId),
keyboardType: TextInputType.numberWithOptions( onChanged: (value) => _onChanged(
decimal: true, signed: true), lineItems[index].rebuild(
onSavePressed: widget.entityViewModel.onSavePressed, (b) => b..quantity = parseDouble(value)),
index),
keyboardType: TextInputType.numberWithOptions(
decimal: true, signed: true),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
if (company.enableProductDiscount) if (company.enableProductDiscount)
Padding( Focus(
padding: const EdgeInsets.only(right: kTableColumnGap), onFocusChange: (hasFocus) => Debouncer.complete(),
child: DecoratedFormField( skipTraversal: true,
key: ValueKey('__line_item_${index}_discount__'), child: Padding(
textAlign: TextAlign.right, padding:
initialValue: formatNumber( const EdgeInsets.only(right: kTableColumnGap),
lineItems[index].discount, context, child: DecoratedFormField(
formatNumberType: FormatNumberType.inputAmount, key: ValueKey('__line_item_${index}_discount__'),
clientId: invoice.clientId), textAlign: TextAlign.right,
onChanged: (value) => _onChanged( initialValue: formatNumber(
lineItems[index].rebuild( lineItems[index].discount, context,
(b) => b..discount = parseDouble(value)), formatNumberType: FormatNumberType.inputAmount,
index), clientId: invoice.clientId),
keyboardType: TextInputType.numberWithOptions( onChanged: (value) => _onChanged(
decimal: true, signed: true), lineItems[index].rebuild(
onSavePressed: widget.entityViewModel.onSavePressed, (b) => b..discount = parseDouble(value)),
index),
keyboardType: TextInputType.numberWithOptions(
decimal: true, signed: true),
onSavePressed: widget.entityViewModel.onSavePressed,
),
), ),
), ),
Padding( Padding(

View File

@ -62,9 +62,14 @@ Completer<Null> errorCompleter(BuildContext context) {
// https://stackoverflow.com/a/55119208/497368 // https://stackoverflow.com/a/55119208/497368
class Debouncer { class Debouncer {
Debouncer({this.milliseconds = kMillisecondsToDebounceUpdate}); Debouncer({
this.milliseconds = kMillisecondsToDebounceUpdate,
this.sendFirstAction = false,
});
final int milliseconds; final int milliseconds;
final bool sendFirstAction;
static VoidCallback action; static VoidCallback action;
static Timer timer; static Timer timer;
@ -75,7 +80,11 @@ class Debouncer {
} }
if (timer == null) { if (timer == null) {
action(); if (sendFirstAction) {
action();
} else {
Debouncer.action = action;
}
} else { } else {
timer.cancel(); timer.cancel();
Debouncer.action = action; Debouncer.action = action;
@ -93,6 +102,7 @@ class Debouncer {
static void complete() { static void complete() {
if (action != null) { if (action != null) {
action(); action();
action = null;
} }
} }