From d72fe69a175dc6daae4aeec2e0336d66c257b11d Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 22 Apr 2019 10:11:58 +0200 Subject: [PATCH] Show more info in product card (closes #173) --- grocy.openapi.json | 8 ++++++ localization/en/strings.php | 4 +++ migrations/0064.sql | 26 +++++++++++++++++++ public/css/grocy.css | 24 +++++++++++++++++ public/viewjs/components/productcard.js | 34 +++++++++++++++++++++++-- services/DemoDataGeneratorService.php | 3 ++- services/StockService.php | 13 +++++++++- views/components/productcard.blade.php | 14 +++++++--- 8 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 migrations/0064.sql diff --git a/grocy.openapi.json b/grocy.openapi.json index 48973de6..59fdb17d 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -2206,6 +2206,14 @@ }, "location": { "$ref": "#/components/schemas/Location" + }, + "average_shelf_life_days": { + "type": "number", + "format": "integer" + }, + "spoil_rate_percent": { + "type": "number", + "format": "double" } } }, diff --git a/localization/en/strings.php b/localization/en/strings.php index 675698d2..92649ed6 100644 --- a/localization/en/strings.php +++ b/localization/en/strings.php @@ -368,4 +368,8 @@ return array( 'Tasks settings' => 'Tasks settings', 'Create shopping list' => 'Create shopping list', 'Are you sure to delete shopping list "#1"?' => 'Are you sure to delete shopping list "#1"?', + 'Average shelf life' => 'Average shelf life', + 'Spoil rate' => 'Spoil rate', + 'Show more' => 'Show more', + 'Show less' => 'Show less' ); diff --git a/migrations/0064.sql b/migrations/0064.sql new file mode 100644 index 00000000..cc1d7d64 --- /dev/null +++ b/migrations/0064.sql @@ -0,0 +1,26 @@ +CREATE VIEW stock_average_product_shelf_life +AS +SELECT + p.id, + CASE WHEN x.product_id IS NULL THEN -1 ELSE AVG(x.shelf_life_days) END AS average_shelf_life_days +FROM products p +LEFT JOIN ( + SELECT + sl_p.product_id, + JULIANDAY(MIN(sl_p.best_before_date)) - JULIANDAY(MIN(sl_c.used_date)) AS shelf_life_days + FROM stock_log sl_p + JOIN ( + SELECT + product_id, + stock_id, + MAX(used_date) AS used_date + FROM stock_log + WHERE transaction_type = 'consume' + GROUP BY product_id, stock_id + ) sl_c + ON sl_p.stock_id = sl_c.stock_id + WHERE sl_p.transaction_type = 'purchase' + GROUP BY sl_p.product_id, sl_p.stock_id + ) x + ON p.id = x.product_id +GROUP BY p.id; diff --git a/public/css/grocy.css b/public/css/grocy.css index 18e4fa38..8f806b6c 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -168,6 +168,30 @@ input::-webkit-inner-spin-button { cursor: wait; } +.expandable-text .collapse, .module .collapsing { + height: 2.4rem; +} + +.expandable-text .collapse { + position: relative; + display: block; + overflow: hidden; +} + +.expandable-text .collapse:before { + position: absolute; + right: 0; + bottom: 0; +} + +.expandable-text .collapse.show { + height: auto; +} + +.expandable-text .collapse.show:before { + display: none; +} + /* Third party component customizations - Bootstrap */ /* Hide the form validation feedback icons introduced in Bootstrap 4.2.0 - a colored border is enough */ diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index 806a8349..76d2b450 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -8,13 +8,33 @@ Grocy.Components.ProductCard.Refresh = function(productId) var stockAmount = productDetails.stock_amount || '0'; var stockAmountOpened = productDetails.stock_amount_opened || '0'; $('#productcard-product-name').text(productDetails.product.name); + $('#productcard-product-description').text(productDetails.product.description); $('#productcard-product-stock-amount').text(stockAmount); - $('#productcard-product-stock-qu-name').text(productDetails.quantity_unit_stock.name); - $('#productcard-product-stock-qu-name2').text(Pluralize(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); + $('#productcard-product-stock-qu-name').text(Pluralize(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); $('#productcard-product-last-purchased').text((productDetails.last_purchased || L('never')).substring(0, 10)); $('#productcard-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || '')); $('#productcard-product-last-used').text((productDetails.last_used || L('never')).substring(0, 10)); $('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); + $('#productcard-product-location').text(productDetails.location.name); + $('#productcard-product-spoil-rate').text(parseFloat(productDetails.spoil_rate_percent).toLocaleString(undefined, { style: "percent" })); + + if (productDetails.product.description != null && !productDetails.product.description.isEmpty()) + { + $("#productcard-product-description-wrapper").removeClass("d-none"); + } + else + { + $("#productcard-product-description-wrapper").addClass("d-none"); + } + + if (productDetails.average_shelf_life_days == -1) + { + $('#productcard-product-average-shelf-life').text(L("Unknown")); + } + else + { + $('#productcard-product-average-shelf-life').text(moment.duration(productDetails.average_shelf_life_days, "days").humanize()); + } if (stockAmountOpened > 0) { @@ -142,3 +162,13 @@ Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() } }); } + +$("#productcard-product-description").on("shown.bs.collapse", function() +{ + $(".expandable-text").find("a[data-toggle='collapse']").text(L("Show less")); +}) + +$("#productcard-product-description").on("hidden.bs.collapse", function() +{ + $(".expandable-text").find("a[data-toggle='collapse']").text(L("Show more")); +}) diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index ac5d0506..457251e5 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -53,7 +53,7 @@ class DemoDataGeneratorService extends BaseService INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Gulash soup')}', 5, 5, 5, 1, 3); --8 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Yogurt')}', 2, 6, 6, 1, 6); --9 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Cheese')}', 2, 3, 3, 1, 6); --10 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->LocalizeForSqlString('Cold cuts')}', 2, 3, 3, 1, 6); --11 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, description) VALUES ('{$localizationService->LocalizeForSqlString('Cold cuts')}', 2, 3, 3, 1, 6, '{$loremIpsum}'); --11 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$localizationService->LocalizeForSqlString('Paprika')}', 2, 2, 2, 1, 5, 'paprika.jpg', 7); --12 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name, default_best_before_days) VALUES ('{$localizationService->LocalizeForSqlString('Cucumber')}', 2, 2, 2, 1, 5, 'cucumber.jpg', 7); --13 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, default_best_before_days) VALUES ('{$localizationService->LocalizeForSqlString('Radish')}', 2, 7, 7, 1, 5, 7); --14 @@ -201,6 +201,7 @@ class DemoDataGeneratorService extends BaseService $stockService->OpenProduct(3, 1); $stockService->OpenProduct(6, 1); $stockService->OpenProduct(22, 1); + $stockService->ConsumeProduct(11, 1, true, StockService::TRANSACTION_TYPE_CONSUME); $choresService = new ChoresService(); $choresService->TrackChore(1, date('Y-m-d H:i:s', strtotime('-5 days'))); diff --git a/services/StockService.php b/services/StockService.php index 3d3b321c..aeab8142 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -79,6 +79,7 @@ class StockService extends BaseService $quPurchase = $this->Database->quantity_units($product->qu_id_purchase); $quStock = $this->Database->quantity_units($product->qu_id_stock); $location = $this->Database->locations($product->location_id); + $averageShelfLifeDays = intval($this->Database->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = null; $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); @@ -87,6 +88,14 @@ class StockService extends BaseService $lastPrice = $lastLogRow->price; } + $consumeCount = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone', 0)->sum('amount') * -1; + $consumeCountSpoiled = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->where('undone = 0 AND spoiled = 1')->sum('amount') * -1; + if ($consumeCount == 0) + { + $consumeCount = 1; + } + $spoilRate = ($consumeCountSpoiled * 100) / $consumeCount; + return array( 'product' => $product, 'last_purchased' => $productLastPurchased, @@ -97,7 +106,9 @@ class StockService extends BaseService 'quantity_unit_stock' => $quStock, 'last_price' => $lastPrice, 'next_best_before_date' => $nextBestBeforeDate, - 'location' => $location + 'location' => $location, + 'average_shelf_life_days' => $averageShelfLifeDays, + 'spoil_rate_percent' => $spoilRate ); } diff --git a/views/components/productcard.blade.php b/views/components/productcard.blade.php index 7e2f1256..ddf72720 100644 --- a/views/components/productcard.blade.php +++ b/views/components/productcard.blade.php @@ -12,11 +12,19 @@

- {{ $L('Stock quantity unit') }}:
- {{ $L('Stock amount') }}:
+ + + + {{ $L('Stock amount') . ' / ' . $L('Quantity unit') }}:
+ {{ $L('Location') }}:
{{ $L('Last purchased') }}:
{{ $L('Last used') }}:
- {{ $L('Last price') }}: + {{ $L('Last price') }}:
+ {{ $L('Average shelf life') }}:
+ {{ $L('Spoil rate') }}:
{{ $L('Product picture') }}