diff --git a/changelog/52_UNRELEASED_2019-xx-xx.md b/changelog/52_UNRELEASED_2019-xx-xx.md index 5d200d3e..d908d315 100644 --- a/changelog/52_UNRELEASED_2019-xx-xx.md +++ b/changelog/52_UNRELEASED_2019-xx-xx.md @@ -20,6 +20,12 @@ - On the quantity unit edit page default conversion can be defined for each unit - Products "inherit" the default conversion and additionally can have their own / override the default ones - It's now possible to print a "Location Content Sheet" with the current stock per location - new button at the top of the stock overview page (thought to hang it at the location, note used amounts on paper and track it in grocy later) +- Stock overview page improvements + - Options in the more/context-menu to directly open the purchase/consume/inventory pages prefilled with the current product in an popup/dialog + - Option in the more/context-menu to add the current product directly to a shopping list + - Option in the more/context-menu to search for recipes containing the current product + - It's now possible to undo stock bookings ("Undo"-button in the success message, like it was already possible on the purchase/consume/inventory pages) + - Improved that on any stock changes the corresponding product table row is properly refreshed - New `config.php` setting `FEATURE_SETTING_STOCK_COUNT_OPENED_PRODUCTS_AGAINST_MINIMUM_STOCK_AMOUNT` to configure if opened products should be considered for minimum stock amounts (defaults to `true`, so opened products will now be considered missing by default - please change this setting if you want the old behaviour) - The product description now can have formattings (HTML/WYSIWYG editor like for recipes) - "Factor purchase to stock quantity unit" (product option) can now also be a decimal number when "Allow partial units in stock" is enabled @@ -34,6 +40,7 @@ - Based on the new linked quantity units, recipe ingredients can now use any product related unit, the amount is calculated according to the cnoversion factor of the unit relation - New option "price factor" per recipe ingredient (defaults to `1`) - the resulting costs of the recipe ingredient will be multiplied by that factor - Use this for example for spices in combination with "Only check if a single unit is in stock" to not take the full price of a pack of pepper into account for a recipe +- The search field on the recipe overview page now also searches for product names of recipe ingredients (means it's possible to search an recipe by a product name) ### Chores improvements - Chores can now be assigned to users @@ -70,6 +77,7 @@ - New endpoint `/stock/products/by-barcode/{barcode}/consume` to remove a product to stock by its barcode - New endpoint `/stock/products/by-barcode/{barcode}/inventory` to inventory a product by its barcode - New endpoint `/stock/products/by-barcode/{barcode}/open` to mark a product as opened by its barcode +- New endpoint `/stock/bookings/{bookingId}` to retrieve a single stock booking - Endpoint `GET /files/{group}/{fileName}` can now also downscale pictures (see API documentation on [/api](https://demo-en.grocy.info/api)) - When adding a product (through `stock/product/{productId}/add` or `stock/product/{productId}/inventory`) with omitted best before date and if the given product has "Default best before days" set, the best before date is calculated based on that (so far always today was used which is still the case when no date is supplied and also the product has no "Default best before days set) (thanks @Forceu) - Field `stock_amount` of endpoint `/stock/products/{productId}` now returns `0` instead of `null` when the given product is not in stock (thanks @Forceu) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 518fa701..7d711e4e 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -448,4 +448,23 @@ class StockApiController extends BaseApiController { return $this->ApiResponse($this->StockService->GetProductStockEntries($args['productId'])); } + + public function StockBooking(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $stockLogRow = $this->Database->stock_log($args['bookingId']); + + if ($stockLogRow === null) + { + throw new \Exception('Stock booking does not exist'); + } + + return $this->ApiResponse($stockLogRow); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index d8038e6c..7491ca1f 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -2037,6 +2037,47 @@ } } }, + "/stock/bookings/{bookingId}": { + "get": { + "summary": "Returns the given stock booking", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "bookingId", + "required": true, + "description": "A valid stock booking id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A StockLogEntry object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockLogEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Invalid stock booking id)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/stock/bookings/{bookingId}/undo": { "post": { "summary": "Undoes a booking", diff --git a/localization/strings.pot b/localization/strings.pot index 7a23b844..41509930 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1480,3 +1480,12 @@ msgstr "" msgid "Say thanks" msgstr "" + +msgid "Search for recipes containing this product" +msgstr "" + +msgid "Add to shopping list" +msgstr "" + +msgid "Added %1$s of %2$s to the shopping list \"%3$s\"" +msgstr "" diff --git a/public/js/grocy.js b/public/js/grocy.js index 8d515c08..44c0014d 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -348,6 +348,11 @@ Grocy.FrontendHelpers = { }; Grocy.FrontendHelpers.ValidateForm = function(formId) { var form = document.getElementById(formId); + if (form === null || form === undefined) + { + return; + } + if (form.checkValidity() === true) { $(form).find(':submit').removeClass('disabled'); @@ -544,3 +549,17 @@ if (!Grocy.CalendarFirstDayOfWeek.isEmpty()) } }); } + +$(window).on("message", function(e) +{ + var data = e.originalEvent.data; + + if (data.Message === "ShowSuccessMessage") + { + toastr.success(data.Payload); + } + else if (data.Message === "CloseAllModals") + { + bootbox.hideAll(); + } +}); diff --git a/public/viewjs/components/barcodescanner.js b/public/viewjs/components/barcodescanner.js index a8a6141a..afbb4645 100644 --- a/public/viewjs/components/barcodescanner.js +++ b/public/viewjs/components/barcodescanner.js @@ -66,6 +66,10 @@ Grocy.Components.BarcodeScanner.StartScanning = function() if (error) { Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message); + setTimeout(function() + { + bootbox.hideAll(); + }, 500); return; } Quagga.start(); @@ -136,8 +140,7 @@ $(document).on("click", "#barcodescanner-start-button", function(e) { Grocy.Components.BarcodeScanner.StopScanning(); } - }, - + } } }); diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 0c9d45e6..1a7bffb4 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -69,30 +69,42 @@ $("#use_specific_stock_entry").click(); } - Grocy.FrontendHelpers.EndUiBusy("consume-form"); if (productDetails.product.enable_tare_weight_handling == 1) { - toastr.success(__t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - parseFloat(productDetails.product.tare_weight)) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''); + var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - parseFloat(productDetails.product.tare_weight)) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; } else { - toastr.success(__t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''); + var successMessage =__t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; } - $("#amount").attr("min", "1"); - $("#amount").attr("max", "999999"); - $("#amount").attr("step", "1"); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '1')); - $('#amount').val(Grocy.UserSettings.stock_default_consume_amount); - $('#amount_qu_unit').text(""); - $("#tare-weight-handling-info").addClass("d-none"); - Grocy.Components.ProductPicker.Clear(); - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES) + if (GetUriParam("embedded") !== undefined) { - Grocy.Components.RecipePicker.Clear(); + window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); + } + else + { + + Grocy.FrontendHelpers.EndUiBusy("consume-form"); + toastr.success(successMessage); + + $("#amount").attr("min", "1"); + $("#amount").attr("max", "999999"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '1')); + $('#amount').val(Grocy.UserSettings.stock_default_consume_amount); + $('#amount_qu_unit').text(""); + $("#tare-weight-handling-info").addClass("d-none"); + Grocy.Components.ProductPicker.Clear(); + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES) + { + Grocy.Components.RecipePicker.Clear(); + } + Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.FrontendHelpers.ValidateForm('consume-form'); } - Grocy.Components.ProductPicker.GetInputElement().focus(); - Grocy.FrontendHelpers.ValidateForm('consume-form'); }, function(xhr) { @@ -275,6 +287,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) }); $('#amount').val(Grocy.UserSettings.stock_default_consume_amount); +Grocy.Components.ProductPicker.GetPicker().trigger('change'); Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.FrontendHelpers.ValidateForm('consume-form'); diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index c6613c06..378085ee 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -64,21 +64,32 @@ Grocy.Api.Get('stock/products/' + jsonForm.product_id, function(result) { - Grocy.FrontendHelpers.EndUiBusy("inventory-form"); - toastr.success(__t('Stock amount of %1$s is now %2$s', result.product.name, result.stock_amount + " " + __n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)) + '
' + __t("Undo") + ''); + var successMessage = __t('Stock amount of %1$s is now %2$s', result.product.name, result.stock_amount + " " + __n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)) + '
' + __t("Undo") + ''; + + if (GetUriParam("embedded") !== undefined) + { + window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); + } + else + { + Grocy.FrontendHelpers.EndUiBusy("inventory-form"); + toastr.success(successMessage); - $('#inventory-change-info').addClass('d-none'); - $("#tare-weight-handling-info").addClass("d-none"); - $("#new_amount").attr("min", "0"); - $("#new_amount").attr("step", "1"); - $("#new_amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '0')); - $('#new_amount').val(''); - $('#new_amount_qu_unit').text(""); - $('#price').val(''); - Grocy.Components.DateTimePicker.Clear(); - Grocy.Components.ProductPicker.SetValue(''); - Grocy.Components.ProductPicker.GetInputElement().focus(); - Grocy.FrontendHelpers.ValidateForm('inventory-form'); + $('#inventory-change-info').addClass('d-none'); + $("#tare-weight-handling-info").addClass("d-none"); + $("#new_amount").attr("min", "0"); + $("#new_amount").attr("step", "1"); + $("#new_amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '0')); + $('#new_amount').val(''); + $('#new_amount_qu_unit').text(""); + $('#price').val(''); + Grocy.Components.DateTimePicker.Clear(); + Grocy.Components.ProductPicker.SetValue(''); + Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.FrontendHelpers.ValidateForm('inventory-form'); + } }, function(xhr) { diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 4bac9879..75b11f5a 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -62,11 +62,13 @@ var successMessage = __t('Added %1$s of %2$s to stock', result.amount + " " +__n(result.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; - if (GetUriParam("flow") === "shoppinglistitemtostock" && typeof GetUriParam("embedded") !== undefined) + if (GetUriParam("embedded") !== undefined) { + window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("AfterItemAdded", GetUriParam("listitemid")), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl); window.parent.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); } else { @@ -105,109 +107,115 @@ ); }); -Grocy.Components.ProductPicker.GetPicker().on('change', function(e) +if (Grocy.Components.ProductPicker !== undefined) { - var productId = $(e.target).val(); - - if (productId) + Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { - Grocy.Components.ProductCard.Refresh(productId); + var productId = $(e.target).val(); - Grocy.Api.Get('stock/products/' + productId, - function (productDetails) - { - $('#price').val(productDetails.last_price); - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) - { - Grocy.Components.LocationPicker.SetId(productDetails.location.id); - } + if (productId) + { + Grocy.Components.ProductCard.Refresh(productId); - if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock) + Grocy.Api.Get('stock/products/' + productId, + function (productDetails) { - $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); - } - else - { - $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied a factor of %1$s to get %2$s", parseInt(productDetails.product.qu_factor_purchase_to_stock).toString(), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")"); - } - - if (productDetails.product.allow_partial_units_in_stock == 1) - { - $("#amount").attr("min", "0.01"); - $("#amount").attr("step", "0.01"); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', 0.01.toLocaleString())); - } - else - { - $("#amount").attr("min", "1"); - $("#amount").attr("step", "1"); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '1')); - } - - if (productDetails.product.enable_tare_weight_handling == 1) - { - var minAmount = parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount) + 1; - $("#amount").attr("min", minAmount); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', minAmount.toLocaleString())); - $("#tare-weight-handling-info").removeClass("d-none"); - } - else - { - $("#tare-weight-handling-info").addClass("d-none"); - } - - if (productDetails.product.default_best_before_days.toString() !== '0') - { - if (productDetails.product.default_best_before_days == -1) + $('#price').val(productDetails.last_price); + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) { - if (!$("#datetimepicker-shortcut").is(":checked")) + Grocy.Components.LocationPicker.SetId(productDetails.location.id); + } + + if (productDetails.product.qu_id_purchase === productDetails.product.qu_id_stock) + { + $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); + } + else + { + $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name + " (" + __t("will be multiplied a factor of %1$s to get %2$s", parseInt(productDetails.product.qu_factor_purchase_to_stock).toString(), __n(2, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)) + ")"); + } + + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '1')); + } + + if (productDetails.product.enable_tare_weight_handling == 1) + { + var minAmount = parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount) + 1; + $("#amount").attr("min", minAmount); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', minAmount.toLocaleString())); + $("#tare-weight-handling-info").removeClass("d-none"); + } + else + { + $("#tare-weight-handling-info").addClass("d-none"); + } + + if (productDetails.product.default_best_before_days.toString() !== '0') + { + if (productDetails.product.default_best_before_days == -1) { - $("#datetimepicker-shortcut").click(); + if (!$("#datetimepicker-shortcut").is(":checked")) + { + $("#datetimepicker-shortcut").click(); + } + } + else + { + Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD')); + } + $('#amount').focus(); + + Grocy.FrontendHelpers.ValidateForm('purchase-form'); + if (GetUriParam("flow") === "shoppinglistitemtostock" && BoolVal(Grocy.UserSettings.shopping_list_to_stock_workflow_auto_submit_when_prefilled) && document.getElementById("purchase-form").checkValidity() === true) + { + $("#save-purchase-button").click(); } } else { - Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD')); + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + { + Grocy.Components.DateTimePicker.GetInputElement().focus(); + } + else + { + Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD')); + $('#amount').focus(); + } } - $('#amount').focus(); - - Grocy.FrontendHelpers.ValidateForm('purchase-form'); - if (GetUriParam("flow") === "shoppinglistitemtostock" && BoolVal(Grocy.UserSettings.shopping_list_to_stock_workflow_auto_submit_when_prefilled) && document.getElementById("purchase-form").checkValidity() === true) - { - $("#save-purchase-button").click(); - } - } - else + }, + function(xhr) { - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) - { - Grocy.Components.DateTimePicker.GetInputElement().focus(); - } - else - { - Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD')); - $('#amount').focus(); - } + console.error(xhr); } - }, - function(xhr) - { - console.error(xhr); - } - ); - } -}); + ); + } + }); +} $('#amount').val(Grocy.UserSettings.stock_default_purchase_amount); Grocy.FrontendHelpers.ValidateForm('purchase-form'); -if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) +if (Grocy.Components.ProductPicker) { - Grocy.Components.ProductPicker.GetInputElement().focus(); -} -else -{ - Grocy.Components.ProductPicker.GetPicker().trigger('change'); + if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) + { + Grocy.Components.ProductPicker.GetInputElement().focus(); + } + else + { + Grocy.Components.ProductPicker.GetPicker().trigger('change'); + } } $('#amount').on('focus', function(e) @@ -244,15 +252,18 @@ $('#purchase-form input').keydown(function(event) } }); -Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e) +if (Grocy.Components.DateTimePicker) { - Grocy.FrontendHelpers.ValidateForm('purchase-form'); -}); + Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e) + { + Grocy.FrontendHelpers.ValidateForm('purchase-form'); + }); -Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) -{ - Grocy.FrontendHelpers.ValidateForm('purchase-form'); -}); + Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) + { + Grocy.FrontendHelpers.ValidateForm('purchase-form'); + }); +} $('#amount').on('change', function(e) { @@ -270,6 +281,17 @@ function UndoStockBooking(bookingId) function(result) { toastr.success(__t("Booking successfully undone")); + + Grocy.Api.Get('stock/bookings/' + bookingId.toString(), + function(result) + { + window.postMessage(WindowMessageBag("ProductChanged", result.product_id), Grocy.BaseUrl); + }, + function (xhr) + { + console.error(xhr); + } + ); }, function(xhr) { diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index 624a87ff..f96c9b38 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -43,6 +43,15 @@ if (typeof recipe !== "undefined") $(cardId)[0].scrollIntoView(); } +if (GetUriParam("search") !== undefined) +{ + $("#search").val(GetUriParam("search")); + setTimeout(function () + { + $("#search").keyup(); + }, 50); +} + $("a[data-toggle='tab']").on("shown.bs.tab", function(e) { var tabId = $(e.target).attr("id"); diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js index e625692b..5ed5403d 100644 --- a/public/viewjs/shoppinglist.js +++ b/public/viewjs/shoppinglist.js @@ -246,10 +246,6 @@ $(window).on("message", function(e) } } } - else if (data.Message === "ShowSuccessMessage") - { - toastr.success(data.Payload); - } }); $(document).on('click', '#shopping-list-stock-add-workflow-skip-button', function(e) diff --git a/public/viewjs/shoppinglistitemform.js b/public/viewjs/shoppinglistitemform.js index c2113aad..49c0aa1c 100644 --- a/public/viewjs/shoppinglistitemform.js +++ b/public/viewjs/shoppinglistitemform.js @@ -10,7 +10,24 @@ Grocy.Api.Post('objects/shopping_list', jsonData, function(result) { - window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); + if (GetUriParam("embedded") !== undefined) + { + Grocy.Api.Get('stock/products/' + jsonData.product_id, + function (productDetails) + { + window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.amount + " " + __n(jsonData.amount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); + }, + function (xhr) + { + console.error(xhr); + } + ); + } + else + { + window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); + } }, function(xhr) { @@ -24,7 +41,24 @@ Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData, function(result) { - window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); + if (GetUriParam("embedded") !== undefined) + { + Grocy.Api.Get('stock/products/' + jsonData.product_id, + function (productDetails) + { + window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", __t("Added %1$s of %2$s to the shopping list \"%3$s\"", jsonData.amount + " " + __n(jsonData.amount, productDetails.quantity_unit_purchase.name, productDetails.quantity_unit_purchase.name_plural), productDetails.product.name, $("#shopping_list_id option:selected").text())), Grocy.BaseUrl); + window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl); + }, + function (xhr) + { + console.error(xhr); + } + ); + } + else + { + window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); + } }, function(xhr) { @@ -74,6 +108,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); Grocy.Components.ProductPicker.GetInputElement().focus(); +Grocy.Components.ProductPicker.GetPicker().trigger('change'); if (Grocy.EditMode === "edit") { diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 316afa46..dea472e8 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -93,70 +93,12 @@ $(document).on('click', '.product-consume-button', function(e) var wasSpoiled = $(e.currentTarget).hasClass("product-consume-button-spoiled"); Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled }, - function() + function(bookingResponse) { Grocy.Api.Get('stock/products/' + productId, function(result) { - var productRow = $('#product-' + productId + '-row'); - var expiringThreshold = moment().add("-" + $("#info-expiring-products").data("next-x-days"), "days"); - var now = moment(); - var nextBestBeforeDate = moment(result.next_best_before_date); - - productRow.removeClass("table-warning"); - productRow.removeClass("table-danger"); - if (now.isAfter(nextBestBeforeDate)) - { - productRow.addClass("table-danger"); - } - if (expiringThreshold.isAfter(nextBestBeforeDate)) - { - productRow.addClass("table-warning"); - } - - var oldAmount = parseFloat($('#product-' + productId + '-amount').text()); - var newAmount = oldAmount - consumeAmount; - if (newAmount <= 0) // When "consume all" of an amount < 1, the resulting amount here will be < 0, but the API newer books > current stock amount - { - $('#product-' + productId + '-row').fadeOut(500, function() - { - $(this).tooltip("hide"); - $(this).remove(); - }); - } - else - { - $('#product-' + productId + '-qu-name').text(__n(newAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)); - $('#product-' + productId + '-amount').parent().effect('highlight', { }, 500); - $('#product-' + productId + '-amount').fadeOut(500, function () - { - $(this).text(newAmount).fadeIn(500); - }); - $('#product-' + productId + '-consume-all-button').attr('data-consume-amount', newAmount); - - $('#product-' + productId + '-next-best-before-date').parent().effect('highlight', { }, 500); - $('#product-' + productId + '-next-best-before-date').fadeOut(500, function() - { - $(this).text(result.next_best_before_date).fadeIn(500); - }); - $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); - - var openedAmount = result.stock_amount_opened || 0; - $('#product-' + productId + '-opened-amount').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-opened-amount').fadeOut(500, function () - { - if (openedAmount > 0) - { - $(this).text(__t('%s opened', openedAmount)).fadeIn(500); - } - else - { - $(this).text("").fadeIn(500); - } - }); - } - - var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name); + var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name) + '
' + __t("Undo") + ''; if (wasSpoiled) { toastMessage += " (" + __t("Spoiled") + ")"; @@ -165,12 +107,7 @@ $(document).on('click', '.product-consume-button', function(e) Grocy.FrontendHelpers.EndUiBusy(); toastr.success(toastMessage); RefreshStatistics(); - - // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... - setTimeout(function () - { - RefreshContextualTimeago(); - }, 520); + RefreshProductRow(productId); }, function(xhr) { @@ -203,54 +140,20 @@ $(document).on('click', '.product-open-button', function(e) var button = $(e.currentTarget); Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': 1 }, - function() + function(bookingResponse) { Grocy.Api.Get('stock/products/' + productId, function(result) { - var productRow = $('#product-' + productId + '-row'); - var expiringThreshold = moment().add("-" + $("#info-expiring-products").data("next-x-days"), "days"); - var now = moment(); - var nextBestBeforeDate = moment(result.next_best_before_date); - - productRow.removeClass("table-warning"); - productRow.removeClass("table-danger"); - if (now.isAfter(nextBestBeforeDate)) - { - productRow.addClass("table-danger"); - } - if (expiringThreshold.isAfter(nextBestBeforeDate)) - { - productRow.addClass("table-warning"); - } - - $('#product-' + productId + '-next-best-before-date').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-next-best-before-date').fadeOut(500, function() - { - $(this).text(result.next_best_before_date).fadeIn(500); - }); - $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); - - $('#product-' + productId + '-opened-amount').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-opened-amount').fadeOut(500, function() - { - $(this).text(__t('%s opened', result.stock_amount_opened)).fadeIn(500); - }); - if (result.stock_amount == result.stock_amount_opened) { button.addClass("disabled"); } Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName)); + toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + ''); RefreshStatistics(); - - // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... - setTimeout(function() - { - RefreshContextualTimeago(); - }, 600); + RefreshProductRow(productId); }, function(xhr) { @@ -304,5 +207,208 @@ function RefreshStatistics() } ); } - RefreshStatistics(); + +$(document).on("click", ".product-purchase-button", function(e) +{ + e.preventDefault(); + + var productId = $(e.currentTarget).attr("data-product-id"); + + bootbox.dialog({ + message: '', + size: 'large', + backdrop: true, + closeButton: false, + buttons: { + cancel: { + label: __t('Cancel'), + className: 'btn-secondary responsive-button', + callback: function() + { + bootbox.hideAll(); + } + } + } + }); +}); + +$(document).on("click", ".product-consume-custom-amount-button", function(e) +{ + e.preventDefault(); + + var productId = $(e.currentTarget).attr("data-product-id"); + + bootbox.dialog({ + message: '', + size: 'large', + backdrop: true, + closeButton: false, + buttons: { + cancel: { + label: __t('Cancel'), + className: 'btn-secondary responsive-button', + callback: function() + { + bootbox.hideAll(); + } + } + } + }); +}); + +$(document).on("click", ".product-inventory-button", function(e) +{ + e.preventDefault(); + + var productId = $(e.currentTarget).attr("data-product-id"); + + bootbox.dialog({ + message: '', + size: 'large', + backdrop: true, + closeButton: false, + buttons: { + cancel: { + label: __t('Cancel'), + className: 'btn-secondary responsive-button', + callback: function() + { + bootbox.hideAll(); + } + } + } + }); +}); + +$(document).on("click", ".product-add-to-shopping-list-button", function(e) +{ + e.preventDefault(); + + var productId = $(e.currentTarget).attr("data-product-id"); + + bootbox.dialog({ + message: '', + size: 'large', + backdrop: true, + closeButton: false, + buttons: { + cancel: { + label: __t('Cancel'), + className: 'btn-secondary responsive-button', + callback: function() + { + bootbox.hideAll(); + } + } + } + }); +}); + +function RefreshProductRow(productId) +{ + productId = productId.toString(); + + Grocy.Api.Get('stock/products/' + productId, + function(result) + { + var productRow = $('#product-' + productId + '-row'); + var expiringThreshold = moment().add("-" + $("#info-expiring-products").data("next-x-days"), "days"); + var now = moment(); + var nextBestBeforeDate = moment(result.next_best_before_date); + + productRow.removeClass("table-warning"); + productRow.removeClass("table-danger"); + if (now.isAfter(nextBestBeforeDate)) + { + productRow.addClass("table-danger"); + } + else if (nextBestBeforeDate.isAfter(expiringThreshold)) + { + productRow.addClass("table-warning"); + } + + if (result.stock_amount <= 0) + { + $('#product-' + productId + '-row').fadeOut(500, function() + { + $(this).tooltip("hide"); + $(this).remove(); + }); + } + else + { + $('#product-' + productId + '-qu-name').text(__n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)); + $('#product-' + productId + '-amount').parent().effect('highlight', { }, 500); + $('#product-' + productId + '-amount').fadeOut(500, function () + { + $(this).text(result.stock_amount).fadeIn(500); + }); + $('#product-' + productId + '-consume-all-button').attr('data-consume-amount', result.stock_amount); + + $('#product-' + productId + '-next-best-before-date').parent().effect('highlight', { }, 500); + $('#product-' + productId + '-next-best-before-date').fadeOut(500, function() + { + $(this).text(result.next_best_before_date).fadeIn(500); + }); + $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); + + var openedAmount = result.stock_amount_opened || 0; + $('#product-' + productId + '-opened-amount').parent().effect('highlight', {}, 500); + $('#product-' + productId + '-opened-amount').fadeOut(500, function () + { + if (openedAmount > 0) + { + $(this).text(__t('%s opened', openedAmount)).fadeIn(500); + } + else + { + $(this).text("").fadeIn(500); + } + }); + } + + $('#product-' + productId + '-next-best-before-date').parent().effect('highlight', {}, 500); + $('#product-' + productId + '-next-best-before-date').fadeOut(500, function() + { + $(this).text(result.next_best_before_date).fadeIn(500); + }); + $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); + + if (result.stock_amount_opened > 0) + { + $('#product-' + productId + '-opened-amount').parent().effect('highlight', {}, 500); + $('#product-' + productId + '-opened-amount').fadeOut(500, function() + { + $(this).text(__t('%s opened', result.stock_amount_opened)).fadeIn(500); + }); + } + else + { + $('#product-' + productId + '-opened-amount').text(""); + } + + // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... + setTimeout(function() + { + RefreshContextualTimeago(); + }, 600); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); +} + +$(window).on("message", function(e) +{ + var data = e.originalEvent.data; + + if (data.Message === "ProductChanged") + { + RefreshProductRow(data.Payload); + RefreshStatistics(); + } +}); diff --git a/routes.php b/routes.php index cb5a6672..104430e0 100644 --- a/routes.php +++ b/routes.php @@ -170,6 +170,7 @@ $app->group('/api', function() $this->post('/stock/products/by-barcode/{barcode}/consume', '\Grocy\Controllers\StockApiController:ConsumeProductByBarcode'); $this->post('/stock/products/by-barcode/{barcode}/inventory', '\Grocy\Controllers\StockApiController:InventoryProductByBarcode'); $this->post('/stock/products/by-barcode/{barcode}/open', '\Grocy\Controllers\StockApiController:OpenProductByBarcode'); + $this->get('/stock/bookings/{bookingId}', '\Grocy\Controllers\StockApiController:StockBooking'); $this->post('/stock/bookings/{bookingId}/undo', '\Grocy\Controllers\StockApiController:UndoBooking'); $this->get('/stock/barcodes/external-lookup', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup'); } diff --git a/views/consume.blade.php b/views/consume.blade.php index bb54450a..e31a4d8f 100644 --- a/views/consume.blade.php +++ b/views/consume.blade.php @@ -60,7 +60,7 @@ -
+
@include('components.productcard')
diff --git a/views/inventory.blade.php b/views/inventory.blade.php index cf1f48ce..312a7bc9 100644 --- a/views/inventory.blade.php +++ b/views/inventory.blade.php @@ -84,7 +84,7 @@ -
+
@include('components.productcard')
diff --git a/views/login.blade.php b/views/login.blade.php index 6fcffd0c..f7830764 100644 --- a/views/login.blade.php +++ b/views/login.blade.php @@ -13,7 +13,6 @@
-
@@ -25,7 +24,7 @@
diff --git a/views/recipes.blade.php b/views/recipes.blade.php index f5032996..a0ca6f95 100644 --- a/views/recipes.blade.php +++ b/views/recipes.blade.php @@ -57,6 +57,7 @@ {{ $__t('Requirements fulfilled') }} Hidden status for sorting of "Requirements fulfilled" column Hidden status for filtering by status + Hidden recipe ingredient product names @include('components.userfields_thead', array( 'userfields' => $userfields @@ -83,6 +84,11 @@ @if(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled == 1) enoughtinstock @elseif(FindObjectInArrayByPropertyValue($recipesResolved, 'recipe_id', $recipe->id)->need_fulfilled_with_shopping_list == 1) enoughinstockwithshoppinglist @else notenoughinstock @endif + + @foreach(FindAllObjectsInArrayByPropertyValue($recipePositionsResolved, 'recipe_id', $recipe->id) as $recipePos) + {{ FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name . ' ' }} + @endforeach + @include('components.userfields_tbody', array( 'userfields' => $userfields, diff --git a/views/shoppinglistitemform.blade.php b/views/shoppinglistitemform.blade.php index 06021d18..8e40da7a 100644 --- a/views/shoppinglistitemform.blade.php +++ b/views/shoppinglistitemform.blade.php @@ -58,7 +58,7 @@
-
+
@include('components.productcard')
diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 851165b0..6d40222b 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -6,6 +6,7 @@ @push('pageScripts') + @endpush @push('pageStyles') @@ -126,6 +127,24 @@