From 4ce7a006df2ee8715dd4c3c3406f53b6e48c258b Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 16 Jun 2024 18:52:26 +0200 Subject: [PATCH] feat: 5203 - "add receipt" and "add price tags", even offline or not found Impacted files: * `app_en.arb`: added 2 "add" and 4 "price product" labels * `app_fr.arb`: added 2 "add" and 4 "price product" labels * `edit_product_page.dart`: minor refactoring * `get_prices_model.dart`: minor refactoring * `price_amount_card.dart`: added the case of "no product yet" * `price_amount_model.dart`: added "no product yet" check * `price_meta_product.dart`: 2 more cases - "no product yet" and "not found product" * `price_product_list_tile.dart`: minor refactoring * `price_product_search_page.dart`: now we accept "not found" products and we don't force the server lookup * `prices_card.dart`: minor refactoring * `product_price_add_page.dart`: minor refactoring * `user_preferences_account.dart`: added "Add receipt" and "Add price tags" --- packages/smooth_app/lib/l10n/app_en.arb | 6 ++ packages/smooth_app/lib/l10n/app_fr.arb | 6 ++ .../preferences/user_preferences_account.dart | 20 +++++ .../lib/pages/prices/get_prices_model.dart | 1 + .../lib/pages/prices/price_amount_card.dart | 36 ++++++-- .../lib/pages/prices/price_amount_model.dart | 6 +- .../lib/pages/prices/price_meta_product.dart | 84 +++++++++++++++---- .../pages/prices/price_product_list_tile.dart | 16 +--- .../prices/price_product_search_page.dart | 31 ++++++- .../lib/pages/prices/prices_card.dart | 1 + .../pages/prices/product_price_add_page.dart | 7 +- .../lib/pages/product/edit_product_page.dart | 1 + 12 files changed, 174 insertions(+), 41 deletions(-) diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 4bf432693ef..620cc39af10 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1676,6 +1676,11 @@ "prices_send_n_prices": "{count,plural, =1{Send the price} other{Send {count} prices}}", "prices_add_an_item": "Add an item", "prices_add_a_price": "Add a price", + "prices_add_a_receipt": "Add a receipt", + "prices_add_price_tags": "Add price tags", + "prices_barcode_search_not_found": "Product not found", + "prices_barcode_search_none_yet": "No product yet", + "prices_barcode_search_question": "Do you want to look for this product?", "prices_barcode_search_title": "Product search", "prices_barcode_search_running": "Looking for {barcode}", "@prices_barcode_search_running": { @@ -1747,6 +1752,7 @@ "prices_amount_price_normal": "Price", "prices_amount_price_discounted": "Discounted price", "prices_amount_price_not_discounted": "Original price", + "prices_amount_no_product": "One product is missing!", "prices_amount_price_incorrect": "Incorrect value", "prices_amount_price_mandatory": "Mandatory value", "prices_currency_subtitle": "Currency", diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index 81c01ace21f..92aa57cbeea 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1678,6 +1678,11 @@ "prices_send_n_prices": "{count,plural, =1{Envoyer le prix} other{Envoyer {count} prix}}", "prices_add_an_item": "Ajouter un article", "prices_add_a_price": "Ajouter un prix", + "prices_add_a_receipt": "Ajouter un ticket de caisse", + "prices_add_price_tags": "Ajouter des étiquettes de prix", + "prices_barcode_search_not_found": "Produit non trouvé", + "prices_barcode_search_none_yet": "Pas encore de produit", + "prices_barcode_search_question": "Voulez-vous chercher ce produit ?", "prices_barcode_search_title": "Recherche de produit", "prices_barcode_search_running": "À la recherche de {barcode}", "@prices_barcode_search_running": { @@ -1749,6 +1754,7 @@ "prices_amount_price_normal": "Prix", "prices_amount_price_discounted": "Prix en promo", "prices_amount_price_not_discounted": "Prix d'origine", + "prices_amount_no_product": "Il manque un produit !", "prices_amount_price_incorrect": "Valeur incorrecte", "prices_amount_price_mandatory": "Valeur obligatoire", "prices_currency_subtitle": "Devise", diff --git a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart index 8104857b724..94ad1b57cb3 100644 --- a/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart +++ b/packages/smooth_app/lib/pages/preferences/user_preferences_account.dart @@ -18,10 +18,12 @@ 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'; import 'package:smooth_app/pages/prices/prices_users_page.dart'; +import 'package:smooth_app/pages/prices/product_price_add_page.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; import 'package:smooth_app/pages/user_management/login_page.dart'; import 'package:smooth_app/query/paged_product_query.dart'; @@ -237,6 +239,24 @@ class UserPreferencesAccount extends AbstractUserPreferences { ), Icons.receipt, ), + _getListTile( + appLocalizations.prices_add_a_receipt, + () async => ProductPriceAddPage.showProductPage( + context: context, + product: PriceMetaProduct.empty(), + proofType: ProofType.receipt, + ), + Icons.add_shopping_cart, + ), + _getListTile( + appLocalizations.prices_add_price_tags, + () async => ProductPriceAddPage.showProductPage( + context: context, + product: PriceMetaProduct.empty(), + proofType: ProofType.priceTag, + ), + Icons.add_shopping_cart, + ), _getListTile( appLocalizations.all_search_prices_latest_title, () async => Navigator.of(context).push( diff --git a/packages/smooth_app/lib/pages/prices/get_prices_model.dart b/packages/smooth_app/lib/pages/prices/get_prices_model.dart index 726b2fee50e..2087975356d 100644 --- a/packages/smooth_app/lib/pages/prices/get_prices_model.dart +++ b/packages/smooth_app/lib/pages/prices/get_prices_model.dart @@ -33,6 +33,7 @@ class GetPricesModel { addButton: () async => ProductPriceAddPage.showProductPage( context: context, product: product, + proofType: ProofType.priceTag, ), enableCountButton: false, ); diff --git a/packages/smooth_app/lib/pages/prices/price_amount_card.dart b/packages/smooth_app/lib/pages/prices/price_amount_card.dart index f5eb0b1d089..314c5209045 100644 --- a/packages/smooth_app/lib/pages/prices/price_amount_card.dart +++ b/packages/smooth_app/lib/pages/prices/price_amount_card.dart @@ -5,8 +5,10 @@ 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 { @@ -36,6 +38,7 @@ class _PriceAmountCardState extends State { @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); + final bool isEmpty = widget.model.product.barcode.isEmpty; return SmoothCard( child: Column( children: [ @@ -45,13 +48,32 @@ class _PriceAmountCardState extends State { ), PriceProductListTile( product: widget.model.product, - trailingIconData: widget.total == 1 ? null : Icons.clear, - onPressed: widget.total == 1 - ? null - : () { - widget.priceModel.priceAmountModels.removeAt(widget.index); - widget.refresh.call(); - }, + trailingIconData: isEmpty + ? Icons.edit + : widget.total == 1 + ? null + : Icons.clear, + onPressed: isEmpty + ? () async { + final PriceMetaProduct? product = + await Navigator.of(context).push( + MaterialPageRoute( + 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(); + }, ), SmoothLargeButtonWithIcon( icon: widget.model.promo diff --git a/packages/smooth_app/lib/pages/prices/price_amount_model.dart b/packages/smooth_app/lib/pages/prices/price_amount_model.dart index 32ec3d100ef..d635342fb33 100644 --- a/packages/smooth_app/lib/pages/prices/price_amount_model.dart +++ b/packages/smooth_app/lib/pages/prices/price_amount_model.dart @@ -33,13 +33,17 @@ class PriceAmountModel { ); String? checkParameters(final BuildContext context) { + final AppLocalizations appLocalizations = AppLocalizations.of(context); + if (product.barcode.isEmpty) { + return appLocalizations.prices_amount_no_product; + } _checkedPaidPrice = validateDouble(_paidPrice)!; _checkedPriceWithoutDiscount = null; if (promo) { if (_priceWithoutDiscount.isNotEmpty) { _checkedPriceWithoutDiscount = validateDouble(_priceWithoutDiscount); if (_checkedPriceWithoutDiscount == null) { - return AppLocalizations.of(context).prices_amount_price_incorrect; + return appLocalizations.prices_amount_price_incorrect; } } } diff --git a/packages/smooth_app/lib/pages/prices/price_meta_product.dart b/packages/smooth_app/lib/pages/prices/price_meta_product.dart index 31af34fed44..cfda26b292e 100644 --- a/packages/smooth_app/lib/pages/prices/price_meta_product.dart +++ b/packages/smooth_app/lib/pages/prices/price_meta_product.dart @@ -1,25 +1,81 @@ +import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; +import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_product_image.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; /// Meta version of a product, coming from OFF or from Prices. class PriceMetaProduct { - PriceMetaProduct.product(Product this.product) : priceProduct = null; - PriceMetaProduct.priceProduct(PriceProduct this.priceProduct) - : product = null; + PriceMetaProduct.product(final Product product) + : _product = product, + _priceProduct = null, + _barcode = null; + PriceMetaProduct.priceProduct(final PriceProduct priceProduct) + : _product = null, + _priceProduct = priceProduct, + _barcode = null; + PriceMetaProduct.empty() + : _product = null, + _priceProduct = null, + _barcode = null; + PriceMetaProduct.unknown(final String barcode) + : _product = null, + _priceProduct = null, + _barcode = barcode; - final Product? product; - final PriceProduct? priceProduct; + final Product? _product; + final PriceProduct? _priceProduct; + final String? _barcode; - String get barcode => - product != null ? product!.barcode! : priceProduct!.code; + // TODO(monsieurtanuki): refine this test + bool get isValid => barcode.length >= 8; - String getName(final AppLocalizations appLocalizations) => product != null - ? getProductNameAndBrands( - product!, - appLocalizations, - ) - : priceProduct!.name ?? priceProduct!.code; + String get barcode { + if (_product != null) { + return _product.barcode!; + } + if (_priceProduct != null) { + return _priceProduct.code; + } + return _barcode ?? ''; + } - String? get imageUrl => product != null ? null : priceProduct!.imageURL; + String getName(final AppLocalizations appLocalizations) { + if (_product != null) { + return getProductNameAndBrands( + _product, + appLocalizations, + ); + } + if (_priceProduct != null) { + return _priceProduct.name ?? _priceProduct.code; + } + if (barcode.isEmpty) { + return appLocalizations.prices_barcode_search_none_yet; + } + return appLocalizations.prices_barcode_search_not_found; + } + + Widget getImageWidget(final double size) { + if (_product != null) { + return SmoothMainProductImage( + product: _product, + width: size, + height: size, + ); + } + if (_priceProduct != null) { + final String? imageURL = _priceProduct.imageURL; + return SmoothImage( + width: size, + height: size, + imageProvider: imageURL == null ? null : NetworkImage(imageURL), + ); + } + return SmoothImage( + width: size, + height: size, + ); + } } diff --git a/packages/smooth_app/lib/pages/prices/price_product_list_tile.dart b/packages/smooth_app/lib/pages/prices/price_product_list_tile.dart index 31cb5f3a0ff..ef3b38d0a30 100644 --- a/packages/smooth_app/lib/pages/prices/price_product_list_tile.dart +++ b/packages/smooth_app/lib/pages/prices/price_product_list_tile.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; -import 'package:smooth_app/generic_lib/widgets/images/smooth_image.dart'; -import 'package:smooth_app/generic_lib/widgets/smooth_product_image.dart'; import 'package:smooth_app/pages/prices/price_meta_product.dart'; /// Displays a meta product with an action button, as a ListTile. @@ -28,19 +26,7 @@ class PriceProductListTile extends StatelessWidget { children: [ SizedBox( width: size, - child: product.product != null - ? SmoothMainProductImage( - product: product.product!, - width: size, - height: size, - ) - : SmoothImage( - width: size, - height: size, - imageProvider: product.imageUrl == null - ? null - : NetworkImage(product.imageUrl!), - ), + child: product.getImageWidget(size), ), const SizedBox(width: SMALL_SPACE), Expanded( diff --git a/packages/smooth_app/lib/pages/prices/price_product_search_page.dart b/packages/smooth_app/lib/pages/prices/price_product_search_page.dart index 74a4933cb59..e16b639b764 100644 --- a/packages/smooth_app/lib/pages/prices/price_product_search_page.dart +++ b/packages/smooth_app/lib/pages/prices/price_product_search_page.dart @@ -7,6 +7,7 @@ import 'package:smooth_app/data_models/fetched_product.dart'; import 'package:smooth_app/database/dao_product.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; +import 'package:smooth_app/generic_lib/dialogs/smooth_alert_dialog.dart'; import 'package:smooth_app/generic_lib/loading_dialog.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_text_form_field.dart'; @@ -50,6 +51,8 @@ class _PriceProductSearchPageState extends State { @override Widget build(BuildContext context) { final AppLocalizations appLocalizations = AppLocalizations.of(context); + final PriceMetaProduct priceMetaProduct = + _product ?? PriceMetaProduct.unknown(_controller.text); // TODO(monsieurtanuki): add WillPopScope2 return SmoothScaffold( appBar: SmoothAppBar( @@ -80,13 +83,13 @@ class _PriceProductSearchPageState extends State { prefixIcon: const Icon(CupertinoIcons.barcode), textInputAction: TextInputAction.search, ), - if (_product != null) + if (priceMetaProduct.isValid) Padding( padding: const EdgeInsets.symmetric(vertical: LARGE_SPACE), child: PriceProductListTile( - product: _product!, + product: priceMetaProduct, trailingIconData: Icons.check_circle, - onPressed: () => Navigator.of(context).pop(_product), + onPressed: () => Navigator.of(context).pop(priceMetaProduct), ), ), ], @@ -167,6 +170,7 @@ class _PriceProductSearchPageState extends State { setState(() => _product = PriceMetaProduct.product(product)); return; } + setState(() {}); } Future _onFieldSubmitted(final BuildContext context) async { @@ -187,6 +191,7 @@ class _PriceProductSearchPageState extends State { } Future _scan(final BuildContext context) async { + final AppLocalizations appLocalizations = AppLocalizations.of(context); final String? barcode = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const PriceScanPage(), @@ -206,6 +211,26 @@ class _PriceProductSearchPageState extends State { if (!context.mounted) { return; } + final bool? accepts = await showDialog( + context: context, + builder: (final BuildContext context) => SmoothAlertDialog( + body: Text(appLocalizations.prices_barcode_search_question), + neutralAction: SmoothActionButton( + text: appLocalizations.cancel, + onPressed: () => Navigator.of(context).pop(false), + ), + positiveAction: SmoothActionButton( + text: appLocalizations.yes, + onPressed: () => Navigator.of(context).pop(true), + ), + ), + ); + if (!context.mounted) { + return; + } + if (accepts != true) { + return; + } await _onFieldSubmitted(context); } } diff --git a/packages/smooth_app/lib/pages/prices/prices_card.dart b/packages/smooth_app/lib/pages/prices/prices_card.dart index 3a5f9acdbe6..f4475c1e5d7 100644 --- a/packages/smooth_app/lib/pages/prices/prices_card.dart +++ b/packages/smooth_app/lib/pages/prices/prices_card.dart @@ -96,6 +96,7 @@ class PricesCard extends StatelessWidget { onPressed: () async => ProductPriceAddPage.showProductPage( context: context, product: PriceMetaProduct.product(product), + proofType: ProofType.priceTag, ), ), ), diff --git a/packages/smooth_app/lib/pages/prices/product_price_add_page.dart b/packages/smooth_app/lib/pages/prices/product_price_add_page.dart index d66d765db05..d851750fc1f 100644 --- a/packages/smooth_app/lib/pages/prices/product_price_add_page.dart +++ b/packages/smooth_app/lib/pages/prices/product_price_add_page.dart @@ -29,14 +29,17 @@ class ProductPriceAddPage extends StatefulWidget { const ProductPriceAddPage({ required this.product, required this.latestOsmLocations, + required this.proofType, }); final PriceMetaProduct product; final List latestOsmLocations; + final ProofType proofType; static Future showProductPage({ required final BuildContext context, required final PriceMetaProduct product, + required final ProofType proofType, }) async { if (!await ProductRefresher().checkIfLoggedIn( context, @@ -58,6 +61,7 @@ class ProductPriceAddPage extends StatefulWidget { builder: (BuildContext context) => ProductPriceAddPage( product: product, latestOsmLocations: osmLocations, + proofType: proofType, ), ), ); @@ -69,7 +73,7 @@ class ProductPriceAddPage extends StatefulWidget { class _ProductPriceAddPageState extends State { late final PriceModel _model = PriceModel( - proofType: ProofType.priceTag, + proofType: widget.proofType, locations: widget.latestOsmLocations, product: widget.product, ); @@ -118,6 +122,7 @@ class _ProductPriceAddPageState extends State { index: i, refresh: () => setState(() {}), ), + // TODO(monsieurtanuki): check if there's an empty barcode before displaying this card SmoothCard( child: SmoothLargeButtonWithIcon( text: appLocalizations.prices_add_an_item, diff --git a/packages/smooth_app/lib/pages/product/edit_product_page.dart b/packages/smooth_app/lib/pages/product/edit_product_page.dart index 610ae38ab5c..83d542fafc9 100644 --- a/packages/smooth_app/lib/pages/product/edit_product_page.dart +++ b/packages/smooth_app/lib/pages/product/edit_product_page.dart @@ -263,6 +263,7 @@ class _EditProductPageState extends State with UpToDateMixin { onTap: () async => ProductPriceAddPage.showProductPage( context: context, product: PriceMetaProduct.product(upToDateProduct), + proofType: ProofType.priceTag, ), ), ],