mirror of
https://github.com/grocy/grocy.git
synced 2025-08-13 17:27:23 +00:00
Added more product actions on the stock overview page (closes #327)
This commit is contained in:
@@ -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)
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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 ""
|
||||
|
@@ -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();
|
||||
}
|
||||
});
|
||||
|
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -69,16 +69,27 @@
|
||||
$("#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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
}
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
}
|
||||
|
||||
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("consume-form");
|
||||
toastr.success(successMessage);
|
||||
|
||||
$("#amount").attr("min", "1");
|
||||
$("#amount").attr("max", "999999");
|
||||
$("#amount").attr("step", "1");
|
||||
@@ -93,6 +104,7 @@
|
||||
}
|
||||
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');
|
||||
|
||||
|
@@ -63,9 +63,19 @@
|
||||
|
||||
Grocy.Api.Get('stock/products/' + jsonForm.product_id,
|
||||
function(result)
|
||||
{
|
||||
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)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
|
||||
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(__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)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
toastr.success(successMessage);
|
||||
|
||||
$('#inventory-change-info').addClass('d-none');
|
||||
$("#tare-weight-handling-info").addClass("d-none");
|
||||
@@ -79,6 +89,7 @@
|
||||
Grocy.Components.ProductPicker.SetValue('');
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
Grocy.FrontendHelpers.ValidateForm('inventory-form');
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
|
@@ -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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
|
||||
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,6 +107,8 @@
|
||||
);
|
||||
});
|
||||
|
||||
if (Grocy.Components.ProductPicker !== undefined)
|
||||
{
|
||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
{
|
||||
var productId = $(e.target).val();
|
||||
@@ -197,10 +201,13 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#amount').val(Grocy.UserSettings.stock_default_purchase_amount);
|
||||
Grocy.FrontendHelpers.ValidateForm('purchase-form');
|
||||
|
||||
if (Grocy.Components.ProductPicker)
|
||||
{
|
||||
if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false)
|
||||
{
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
@@ -209,6 +216,7 @@ else
|
||||
{
|
||||
Grocy.Components.ProductPicker.GetPicker().trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
$('#amount').on('focus', function(e)
|
||||
{
|
||||
@@ -244,6 +252,8 @@ $('#purchase-form input').keydown(function(event)
|
||||
}
|
||||
});
|
||||
|
||||
if (Grocy.Components.DateTimePicker)
|
||||
{
|
||||
Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e)
|
||||
{
|
||||
Grocy.FrontendHelpers.ValidateForm('purchase-form');
|
||||
@@ -253,6 +263,7 @@ 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)
|
||||
{
|
||||
|
@@ -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");
|
||||
|
@@ -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)
|
||||
|
@@ -9,8 +9,25 @@
|
||||
{
|
||||
Grocy.Api.Post('objects/shopping_list', jsonData,
|
||||
function(result)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -23,8 +40,25 @@
|
||||
{
|
||||
Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData,
|
||||
function(result)
|
||||
{
|
||||
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")
|
||||
{
|
||||
|
@@ -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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
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: '<iframe height="650px" class="embed-responsive" src="' + U("/purchase?embedded&product=") + productId.toString() + '"></iframe>',
|
||||
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: '<iframe height="650px" class="embed-responsive" src="' + U("/consume?embedded&product=") + productId.toString() + '"></iframe>',
|
||||
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: '<iframe height="650px" class="embed-responsive" src="' + U("/inventory?embedded&product=") + productId.toString() + '"></iframe>',
|
||||
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: '<iframe height="650px" class="embed-responsive" src="' + U("/shoppinglistitem/new?embedded&product=") + productId.toString() + '"></iframe>',
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -60,7 +60,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-xl-4">
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 hide-when-embedded">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -84,7 +84,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-xl-4">
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 hide-when-embedded">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -13,7 +13,6 @@
|
||||
<div class="form-group">
|
||||
<label for="name">{{ $__t('Username') }}</label>
|
||||
<input type="text" class="form-control" required id="username" name="username">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -25,7 +24,7 @@
|
||||
<div class="checkbox">
|
||||
<label for="stay_logged_in">
|
||||
<input type="checkbox" id="stay_logged_in" name="stay_logged_in"> {{ $__t('Stay logged in permanently') }}
|
||||
<p id="qu-conversion-info" class="form-text text-muted small my-0">{{ $__t('When not set, you will get logged out at latest after 30 days') }}</p>
|
||||
<p class="form-text text-muted small my-0">{{ $__t('When not set, you will get logged out at latest after 30 days') }}</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@@ -57,6 +57,7 @@
|
||||
<th>{{ $__t('Requirements fulfilled') }}</th>
|
||||
<th class="d-none">Hidden status for sorting of "Requirements fulfilled" column</th>
|
||||
<th class="d-none">Hidden status for filtering by status</th>
|
||||
<th class="d-none">Hidden recipe ingredient product names</th>
|
||||
|
||||
@include('components.userfields_thead', array(
|
||||
'userfields' => $userfields
|
||||
@@ -83,6 +84,11 @@
|
||||
<td class="d-none">
|
||||
@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
|
||||
</td>
|
||||
<td class="d-none">
|
||||
@foreach(FindAllObjectsInArrayByPropertyValue($recipePositionsResolved, 'recipe_id', $recipe->id) as $recipePos)
|
||||
{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name . ' ' }}
|
||||
@endforeach
|
||||
</td>
|
||||
|
||||
@include('components.userfields_tbody', array(
|
||||
'userfields' => $userfields,
|
||||
|
@@ -58,7 +58,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-xl-4">
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 hide-when-embedded">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -6,6 +6,7 @@
|
||||
|
||||
@push('pageScripts')
|
||||
<script src="{{ $U('/node_modules/jquery-ui-dist/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
|
||||
<script src="{{ $U('/viewjs/purchase.js?v=', true) }}{{ $version }}"></script>
|
||||
@endpush
|
||||
|
||||
@push('pageStyles')
|
||||
@@ -126,6 +127,24 @@
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item product-add-to-shopping-list-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-shopping-cart"></i> {{ $__t('Add to shopping list') }}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item product-purchase-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-shopping-cart"></i> {{ $__t('Purchase') }}
|
||||
</a>
|
||||
<a class="dropdown-item product-consume-custom-amount-button @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-utensils"></i> {{ $__t('Consume') }}
|
||||
</a>
|
||||
<a class="dropdown-item product-inventory-button @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-list"></i> {{ $__t('Inventory') }}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item product-name-cell" data-product-id="{{ $currentStockEntry->product_id }}" type="button" href="#">
|
||||
<i class="fas fa-info"></i> {{ $__t('Show product details') }}
|
||||
</a>
|
||||
@@ -143,6 +162,9 @@
|
||||
data-consume-amount="1">
|
||||
<i class="fas fa-utensils"></i> {{ $__t('Consume %1$s of %2$s as spoiled', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}
|
||||
</a>
|
||||
<a class="dropdown-item" type="button" href="{{ $U('/recipes?search=') }}{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}">
|
||||
<i class="fas fa-cocktail"></i> {{ $__t('Search for recipes containing this product') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
Reference in New Issue
Block a user