Skip to content

Commit

Permalink
feat: improved price product selection
Browse files Browse the repository at this point in the history
Deleted files:
* `price_product_search_page.dart`

New file:
* `price_add_product_card.dart`: Card where the user can input a price product: type the barcode or scan.

Impacted files:
* `price_amount_card.dart`: removed the "no product" option; refactored Provider and Controllers
* `price_amount_model.dart`: minor refactoring
* `price_meta_product.dart`: removed the "no product" option; async load of data (local+server)
* `price_model.dart`: minor refactoring
* `product_price_add_page.dart`: refactored Provider; now using new widget `PriceAddProductCard`
* `user_preferences_account.dart`: minor refactoring
  • Loading branch information
monsieurtanuki committed Aug 30, 2024
1 parent c50a10c commit 2229069
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 426 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import 'package:smooth_app/pages/preferences/user_preferences_item.dart';
import 'package:smooth_app/pages/preferences/user_preferences_list_tile.dart';
import 'package:smooth_app/pages/preferences/user_preferences_page.dart';
import 'package:smooth_app/pages/prices/get_prices_model.dart';
import 'package:smooth_app/pages/prices/price_meta_product.dart';
import 'package:smooth_app/pages/prices/price_user_button.dart';
import 'package:smooth_app/pages/prices/prices_page.dart';
import 'package:smooth_app/pages/prices/prices_proofs_page.dart';
Expand Down Expand Up @@ -243,7 +242,6 @@ class UserPreferencesAccount extends AbstractUserPreferences {
appLocalizations.prices_add_a_receipt,
() async => ProductPriceAddPage.showProductPage(
context: context,
product: PriceMetaProduct.empty(),
proofType: ProofType.receipt,
),
Icons.add_shopping_cart,
Expand All @@ -252,7 +250,6 @@ class UserPreferencesAccount extends AbstractUserPreferences {
appLocalizations.prices_add_price_tags,
() async => ProductPriceAddPage.showProductPage(
context: context,
product: PriceMetaProduct.empty(),
proofType: ProofType.priceTag,
),
Icons.add_shopping_cart,
Expand Down
142 changes: 142 additions & 0 deletions packages/smooth_app/lib/pages/prices/price_add_product_card.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart';
import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart';
import 'package:smooth_app/pages/prices/price_amount_model.dart';
import 'package:smooth_app/pages/prices/price_meta_product.dart';
import 'package:smooth_app/pages/prices/price_model.dart';
import 'package:smooth_app/pages/prices/price_scan_page.dart';

/// Card where the user can input a price product: type the barcode or scan.
class PriceAddProductCard extends StatelessWidget {
const PriceAddProductCard();

static const TextInputType _textInputType = TextInputType.number;

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
return SmoothCard(
child: Column(
children: <Widget>[
ListTile(
title: Text(
appLocalizations.prices_add_an_item,
),
),
SmoothLargeButtonWithIcon(
text: appLocalizations.barcode,
icon: Icons.text_fields,
onPressed: () async {
final TextEditingController controller = TextEditingController();
final String? barcode = await showDialog<String>(
context: context,
builder: (final BuildContext context) => StatefulBuilder(
builder: (
final BuildContext context,
void Function(VoidCallback fn) setState,
) =>
SmoothAlertDialog(
title: appLocalizations.prices_add_an_item,
body: SmoothTextFormField(
autofocus: true,
type: TextFieldTypes.PLAIN_TEXT,
controller: controller,
hintText: appLocalizations.barcode,
textInputType: _textInputType,
onChanged: (_) {
final String barcode = controller.text;
final String cleanBarcode = _getCleanBarcode(barcode);
setState(() => controller.text = cleanBarcode);
},
onFieldSubmitted: (_) => !_isValidBarcode(controller.text)
? null
: Navigator.of(context).pop(controller.text),
),
positiveAction: SmoothActionButton(
text: appLocalizations.validate,
onPressed: !_isValidBarcode(controller.text)
? null
: () => Navigator.of(context).pop(controller.text),
),
negativeAction: SmoothActionButton(
text: appLocalizations.cancel,
onPressed: () => Navigator.of(context).pop(),
),
),
),
);
if (barcode == null) {
return;
}
if (!context.mounted) {
return;
}
await _addToList(barcode, context);
},
),
SmoothLargeButtonWithIcon(
text: appLocalizations.prices_barcode_reader_action,
icon: Icons.barcode_reader,
onPressed: () async {
final String? barcode = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(
builder: (BuildContext context) => const PriceScanPage(),
),
);
if (barcode == null) {
return;
}
if (!context.mounted) {
return;
}
await _addToList(barcode, context);
},
),
],
),
);
}

Future<void> _addToList(
final String barcode,
final BuildContext context,
) async {
final LocalDatabase localDatabase = context.read<LocalDatabase>();
final PriceModel priceModel = Provider.of<PriceModel>(
context,
listen: false,
);
priceModel.priceAmountModels.add(
PriceAmountModel(
product: PriceMetaProduct.unknown(
barcode,
localDatabase,
priceModel,
),
),
);
priceModel.notifyListeners();
}

bool _isValidBarcode(final String barcode) => barcode.length >= 8;

// Probably there's a regexp for that, but at least it's readable code.
String _getCleanBarcode(final String input) {
const int ascii0 = 48;
const int ascii9 = 48 + 10 - 1;

final StringBuffer buffer = StringBuffer();
for (int i = 0; i < input.length; i++) {
final int charCode = input.codeUnitAt(i);
if (charCode >= ascii0 && charCode <= ascii9) {
buffer.writeCharCode(charCode);
}
}
return buffer.toString();
}
}
94 changes: 43 additions & 51 deletions packages/smooth_app/lib/pages/prices/price_amount_card.dart
Original file line number Diff line number Diff line change
@@ -1,87 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/pages/prices/price_amount_field.dart';
import 'package:smooth_app/pages/prices/price_amount_model.dart';
import 'package:smooth_app/pages/prices/price_meta_product.dart';
import 'package:smooth_app/pages/prices/price_model.dart';
import 'package:smooth_app/pages/prices/price_product_list_tile.dart';
import 'package:smooth_app/pages/prices/price_product_search_page.dart';

/// Card that displays the amounts (discounted or not) for price adding.
class PriceAmountCard extends StatefulWidget {
PriceAmountCard({
required this.priceModel,
const PriceAmountCard({
required this.index,
required this.refresh,
}) : model = priceModel.priceAmountModels[index],
total = priceModel.priceAmountModels.length;
required super.key,
});

final PriceModel priceModel;
final PriceAmountModel model;
final int index;
final int total;
// TODO(monsieurtanuki): not elegant, the display was not refreshed when removing an item
final VoidCallback refresh;

@override
State<PriceAmountCard> createState() => _PriceAmountCardState();
}

class _PriceAmountCardState extends State<PriceAmountCard> {
final TextEditingController _controllerPaid = TextEditingController();
final TextEditingController _controllerWithoutDiscount =
TextEditingController();
late final TextEditingController _controllerPaid;
late final TextEditingController _controllerWithoutDiscount;

@override
void initState() {
super.initState();
final PriceAmountModel model = Provider.of<PriceModel>(
context,
listen: false,
).priceAmountModels[widget.index];
_controllerPaid = TextEditingController(text: model.paidPrice);
_controllerWithoutDiscount =
TextEditingController(text: model.priceWithoutDiscount);
}

@override
void dispose() {
_controllerPaid.dispose();
_controllerWithoutDiscount.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final bool isEmpty = widget.model.product.barcode.isEmpty;
final PriceModel priceModel = Provider.of<PriceModel>(context);
final PriceAmountModel model = priceModel.priceAmountModels[widget.index];
final int total = priceModel.priceAmountModels.length;

return SmoothCard(
child: Column(
children: <Widget>[
Text(
'${appLocalizations.prices_amount_subtitle}'
'${widget.total == 1 ? '' : ' (${widget.index + 1}/${widget.total})'}',
'${total == 1 ? '' : ' (${widget.index + 1}/$total)'}',
),
PriceProductListTile(
product: widget.model.product,
trailingIconData: isEmpty
? Icons.edit
: widget.total == 1
? null
: Icons.clear,
onPressed: isEmpty
? () async {
final PriceMetaProduct? product =
await Navigator.of(context).push<PriceMetaProduct>(
MaterialPageRoute<PriceMetaProduct>(
builder: (BuildContext context) =>
const PriceProductSearchPage(),
),
);
if (product == null) {
return;
}
setState(() => widget.model.product = product);
}
: widget.total == 1
? null
: () {
widget.priceModel.priceAmountModels
.removeAt(widget.index);
widget.refresh.call();
},
product: model.product,
trailingIconData: total == 1 ? null : Icons.clear,
onPressed: total == 1
? null
: () {
priceModel.priceAmountModels.removeAt(widget.index);
priceModel.notifyListeners();
},
),
SmoothLargeButtonWithIcon(
icon: widget.model.promo
? Icons.check_box
: Icons.check_box_outline_blank,
icon: model.promo ? Icons.check_box : Icons.check_box_outline_blank,
text: appLocalizations.prices_amount_is_discounted,
onPressed: () => setState(
() => widget.model.promo = !widget.model.promo,
() => model.promo = !model.promo,
),
),
const SizedBox(height: SMALL_SPACE),
Expand All @@ -91,17 +83,17 @@ class _PriceAmountCardState extends State<PriceAmountCard> {
child: PriceAmountField(
controller: _controllerPaid,
isPaidPrice: true,
model: widget.model,
model: model,
),
),
const SizedBox(width: LARGE_SPACE),
Expanded(
child: !widget.model.promo
child: !model.promo
? Container()
: PriceAmountField(
controller: _controllerWithoutDiscount,
isPaidPrice: false,
model: widget.model,
model: model,
),
),
],
Expand Down
14 changes: 5 additions & 9 deletions packages/smooth_app/lib/pages/prices/price_amount_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,8 @@ class PriceAmountModel {

PriceMetaProduct product;

String _paidPrice = '';
String _priceWithoutDiscount = '';

set paidPrice(final String value) => _paidPrice = value;

set priceWithoutDiscount(final String value) => _priceWithoutDiscount = value;
String paidPrice = '';
String priceWithoutDiscount = '';

late double _checkedPaidPrice;
double? _checkedPriceWithoutDiscount;
Expand All @@ -37,11 +33,11 @@ class PriceAmountModel {
if (product.barcode.isEmpty) {
return appLocalizations.prices_amount_no_product;
}
_checkedPaidPrice = validateDouble(_paidPrice)!;
_checkedPaidPrice = validateDouble(paidPrice)!;
_checkedPriceWithoutDiscount = null;
if (promo) {
if (_priceWithoutDiscount.isNotEmpty) {
_checkedPriceWithoutDiscount = validateDouble(_priceWithoutDiscount);
if (priceWithoutDiscount.isNotEmpty) {
_checkedPriceWithoutDiscount = validateDouble(priceWithoutDiscount);
if (_checkedPriceWithoutDiscount == null) {
return appLocalizations.prices_amount_price_incorrect;
}
Expand Down
Loading

0 comments on commit 2229069

Please sign in to comment.