Grocy.Components.ProductPicker = {};
Grocy.Components.ProductPicker.GetPicker = function() {
return $('#product_id');
}
Grocy.Components.ProductPicker.GetValue = function() {
return this.GetPicker().val();
}
Grocy.Components.ProductPicker.GetOption = function(key) {
return this.GetPicker().parents('.form-group').data(key);
}
Grocy.Components.ProductPicker.GetState = function(key) {
return this.GetPicker().data(key);
}
Grocy.Components.ProductPicker.SetState = function(key, value) {
this.GetPicker().data(key, value);
return this;
}
Grocy.Components.ProductPicker.SetId = function(value, callback) {
if (this.GetPicker().find('option[value="' + value + '"]').length) {
this.GetPicker().val(value).trigger('change');
} else {
Grocy.Api.Get('objects/products/' + encodeURIComponent(value),
function(result) {
var option = new Option(result.name, value, true, true);
if (typeof callback === 'function') {
Grocy.Components.ProductPicker.GetPicker().one('change.select2', callback);
}
Grocy.Components.ProductPicker.GetPicker().append(option).trigger('change').select2('close');
},
function(xhr) {
console.error(xhr);
}
);
}
return this;
}
Grocy.Components.ProductPicker.Clear = function() {
this.GetPicker().val(null).trigger('change');
return this;
}
Grocy.Components.ProductPicker.InProductAddWorkflow = function() {
return GetUriParam('flow') == "InplaceNewProductWithName";
}
Grocy.Components.ProductPicker.InProductModifyWorkflow = function() {
return GetUriParam('flow') == "InplaceAddBarcodeToExistingProduct";
}
Grocy.Components.ProductPicker.InAnyFlow = function() {
return GetUriParam('flow') !== undefined;
}
Grocy.Components.ProductPicker.FinishFlow = function() {
RemoveUriParam("flow");
RemoveUriParam("barcode");
RemoveUriParam("product-name");
return this;
}
Grocy.Components.ProductPicker.ShowCustomError = function(text) {
var element = $("#custom-productpicker-error");
element.text(text);
element.removeClass("d-none");
return this;
}
Grocy.Components.ProductPicker.HideCustomError = function() {
$("#custom-productpicker-error").addClass("d-none");
return this;
}
Grocy.Components.ProductPicker.Disable = function() {
this.GetPicker().prop("disabled", true);
$("#barcodescanner-start-button").attr("disabled", "");
$("#barcodescanner-start-button").addClass("disabled");
return this;
}
Grocy.Components.ProductPicker.Enable = function() {
this.GetPicker().prop("disabled", false);
$("#barcodescanner-start-button").removeAttr("disabled");
$("#barcodescanner-start-button").removeClass("disabled");
return this;
}
Grocy.Components.ProductPicker.IsRequired = function() {
return this.GetPicker().prop("required");
}
Grocy.Components.ProductPicker.Require = function() {
this.GetPicker().prop("required", true);
return this;
}
Grocy.Components.ProductPicker.Optional = function() {
this.GetPicker().prop("required", false);
return this;
}
Grocy.Components.ProductPicker.Focus = function() {
this.GetPicker().select2('open');
return this;
}
Grocy.Components.ProductPicker.Validate = function() {
this.GetPicker().trigger('change');
return this;
}
Grocy.Components.ProductPicker.OnChange = function(eventHandler) {
this.GetPicker().on('change', eventHandler);
return this;
}
Grocy.Components.ProductPicker.Search = function(term, callback) {
var $picker = this.GetPicker();
var doSearch = function() {
var $search = $picker.data('select2').dropdown.$search || $picker.data('select2').selection.$search;
$search.val(term).trigger('input');
// must wait for debounce before listening for 'results:all' event
setTimeout(function() {
$picker.one('Grocy.ResultsUpdated', function() {
if (typeof callback === 'function') {
$picker.one('select2:close', callback);
}
$picker.select2('close');
});
}, 150);
};
if ($picker.data('select2').isOpen()) {
doSearch();
} else {
$picker.one('select2:open', doSearch);
$picker.select2('open');
}
return this;
}
Grocy.Components.ProductPicker.IsOpen = function() {
return this.GetPicker().parent().find('.select2-container').hasClass('select2-container--open');
}
// initialize Select2 product picker
var lastProductSearchTerm = '';
Grocy.Components.ProductPicker.GetPicker().select2({
placeholder: Grocy.Components.ProductPicker.IsRequired() ? null : __t('All'),
placeholderOption: 'all',
selectOnClose: Grocy.Components.ProductPicker.IsRequired(),
allowClear: !Grocy.Components.ProductPicker.IsRequired(),
ajax: {
delay: 150,
transport: function(params, success, failure) {
var results_per_page = 10;
var page = params.data.page || 1;
var term = params.data.term || "";
var termIsGrocycode = term.startsWith("grcy");
lastProductSearchTerm = term;
// reset grocycode/barcode state
Grocy.Components.ProductPicker.SetState('barcode', 'null');
Grocy.Components.ProductPicker.SetState('grocycode', false);
// build search queries
var baseQuery = Grocy.Components.ProductPicker.GetOption('products-query').split('&');
baseQuery.push('limit=' + encodeURIComponent(results_per_page));
baseQuery.push('offset=' + encodeURIComponent((page - 1) * results_per_page));
var queries = [];
if (term.length > 0) {
queries = [
// search product fields (name, etc.)
baseQuery.concat('search=' + encodeURIComponent(term)).join('&'),
];
// grocycode handling
if (termIsGrocycode) {
var gc = term.split(":");
if (gc[1] == "p") {
queries.push(baseQuery.concat('query%5B%5D=id%3D' + encodeURIComponent(gc[2])).join('&'));
}
}
} else {
queries = [baseQuery.join('&')];
}
// execute all queries in parallel, return first non-empty response
var complete = 0;
var responded = false;
var xhrs = [];
var handleEmptyResponse = function() {
if (responded || complete < xhrs.length) return;
success({
results: Grocy.Components.ProductPicker.IsRequired() ? [] : [{
id: 'all',
text: __t('All'),
}],
pagination: {
more: false
}
});
};
var handleResponse = function(results, meta, cur_xhr) {
// track complete queries
complete++;
// abort if we already responded
if (responded) return;
// abort if no results
if (results.length === 0) {
handleEmptyResponse();
return;
}
// track whether we have responded
responded = true;
// abort all other queries
xhrs.forEach(function(xhr) {
if (xhr !== cur_xhr) xhr.abort();
});
// update grocycode/barcode state
Grocy.Components.ProductPicker.SetState('grocycode', termIsGrocycode);
Grocy.Components.ProductPicker.SetState('barcode', term);
success({
results: (Grocy.Components.ProductPicker.IsRequired() ? [] : [{
id: 'all',
text: __t('All'),
}]).concat(results.map(function(result) {
return {
id: result.id,
text: result.name
};
})),
pagination: {
more: page * results_per_page < meta.recordsFiltered
}
});
};
var handleErrors = function(xhr) {
console.error(xhr);
// track complete queries
complete++;
// abort if we already responded
if (responded) return;
handleEmptyResponse();
};
if (term.length > 0) {
xhrs.push(Grocy.Api.Get('objects/product_barcodes?limit=1&query%5B%5D=barcode%3D' + encodeURIComponent(term),
function(results, meta) {
// track complete queries
complete++;
// abort if we already responded
if (responded) return;
// abort if no results
if (results.length === 0) {
handleEmptyResponse();
return;
}
var cur_xhr = Grocy.Api.Get('objects/products?' + baseQuery.concat('query%5B%5D=id%3D' + encodeURIComponent(results[0].product_id)).join('&'),
function(results, meta) {
handleResponse(results, meta, cur_xhr);
},
handleErrors
);
xhrs.push(cur_xhr);
},
handleErrors
));
}
xhrs = xhrs.concat(queries.map(function(query) {
var cur_xhr = Grocy.Api.Get('objects/products' + (query.length > 0 ? '?' + query : ''),
function(results, meta) {
handleResponse(results, meta, cur_xhr);
},
handleErrors
);
return cur_xhr;
}));
}
}
});
// forward 'results:all' event
Grocy.Components.ProductPicker.GetPicker().data('select2').on('results:all', function(data) {
Grocy.Components.ProductPicker.GetPicker().trigger('Grocy.ResultsUpdated', data);
});
// handle barcode scanning
$(document).on("Grocy.BarcodeScanned", function(e, barcode, target) {
// check that the barcode scan is for the product picker
if (!(target == "@productpicker" || target == "undefined" || target == undefined)) return;
Grocy.Components.ProductPicker.Search(barcode);
});
// fix placement of bootbox footer buttons
$(document).on("shown.bs.modal", function() {
$(".modal-footer").addClass("d-block").addClass("d-sm-flex");
$(".modal-footer").find("button").addClass("mt-2").addClass("mt-sm-0");
});
if (GetUriParam("flow") === "InplaceAddBarcodeToExistingProduct") {
$('#InplaceAddBarcodeToExistingProduct').text(GetUriParam("barcode"));
$('#flow-info-InplaceAddBarcodeToExistingProduct').removeClass('d-none');
$('#barcode-lookup-disabled-hint').removeClass('d-none');
$('#barcode-lookup-hint').addClass('d-none');
}
// prefill by name
var prefillProduct = Grocy.Components.ProductPicker.GetOption('prefill-by-name') || GetUriParam('product-name');
if (typeof prefillProduct === 'string' && !prefillProduct.isEmpty()) {
Grocy.Components.ProductPicker.Search(prefillProduct, function() {
$(Grocy.Components.ProductPicker.GetOption('next-input-selector')).trigger('focus');
});
}
// prefill by ID
var prefillProduct = Grocy.Components.ProductPicker.GetOption('prefill-by-id') || GetUriParam('product');
if (typeof prefillProduct === 'string' && !prefillProduct.isEmpty()) {
Grocy.Components.ProductPicker.SetId(prefillProduct, function() {
$(Grocy.Components.ProductPicker.GetOption('next-input-selector')).trigger('focus');
});
}
// open create product/barcode dialog if no results
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.GetPicker().on('select2:close', function() {
if (Grocy.Components.ProductPicker.PopupOpen || Grocy.Components.ProductPicker.GetPicker().select2('data').length > 0) return;
var addProductWorkflowsAdditionalCssClasses = "";
if (Grocy.Components.ProductPicker.GetOption('disallow-add-product-workflows')) {
addProductWorkflowsAdditionalCssClasses = "d-none";
}
var embedded = "";
if (GetUriParam("embedded") !== undefined) {
embedded = "embedded";
}
var buttons = {
cancel: {
label: __t('Cancel'),
className: 'btn-secondary responsive-button',
callback: function() {
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.Clear();
}
},
addnewproduct: {
label: 'P ' + __t('Add as new product'),
className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
callback: function() {
// Not the best place here - this is only relevant when this flow is started from the shopping list item form
// (to select the correct shopping list on return)
if (GetUriParam("list") !== undefined) {
embedded += "&list=" + GetUriParam("list");
}
Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U('/product/new?flow=InplaceNewProductWithName&name=' + encodeURIComponent(lastProductSearchTerm) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName&" + embedded) + "&" + embedded);
}
},
addbarcode: {
label: 'B ' + __t('Add as barcode to existing product'),
className: 'btn-info add-new-barcode-dialog-button responsive-button',
callback: function() {
Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U(Grocy.CurrentUrlRelative + '?flow=InplaceAddBarcodeToExistingProduct&barcode=' + encodeURIComponent(lastProductSearchTerm) + "&" + embedded);
}
},
addnewproductwithbarcode: {
label: 'A ' + __t('Add as new product and prefill barcode'),
className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
callback: function() {
Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(lastProductSearchTerm) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + lastProductSearchTerm + "&" + embedded) + "&" + embedded);
}
}
};
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_DISABLE_BROWSER_BARCODE_CAMERA_SCANNING) {
buttons.retrycamerascanning = {
label: 'C ',
className: 'btn-primary responsive-button retry-camera-scanning-button',
callback: function() {
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.Clear();
$("#barcodescanner-start-button").trigger('click');
}
};
}
// The product picker contains only in-stock products on some pages,
// so only show the workflow dialog when the entered input
// does not match in existing product (name) or barcode,
// otherwise an error validation message that the product is not in stock
var existsAsProduct = false;
var existsAsBarcode = false;
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + lastProductSearchTerm,
function(barcodeResult) {
if (barcodeResult.length > 0) {
existsAsProduct = true;
}
Grocy.Api.Get('objects/products?query[]=name=' + lastProductSearchTerm,
function(productResult) {
if (productResult.length > 0) {
existsAsProduct = true;
}
if (!existsAsBarcode && !existsAsProduct) {
Grocy.Components.ProductPicker.PopupOpen = true;
bootbox.dialog({
message: __t('"%s" could not be resolved to a product, how do you want to proceed?', lastProductSearchTerm),
title: __t('Create or assign product'),
onEscape: function() {
Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.Components.ProductPicker.Clear();
},
size: 'large',
backdrop: true,
closeButton: false,
buttons: buttons
}).on('keypress', function(e) {
if (e.key === 'B' || e.key === 'b') {
$('.add-new-barcode-dialog-button').not(".d-none").trigger('click');
}
if (e.key === 'p' || e.key === 'P') {
$('.add-new-product-dialog-button').not(".d-none").trigger('click');
}
if (e.key === 'a' || e.key === 'A') {
$('.add-new-product-with-barcode-dialog-button').not(".d-none").trigger('click');
}
if (e.key === 'c' || e.key === 'C') {
$('.retry-camera-scanning-button').not(".d-none").trigger('click');
}
});
} else {
Grocy.Components.ProductAmountPicker.Reset();
Grocy.Components.ProductPicker.Clear();
Grocy.FrontendHelpers.ValidateForm('consume-form');
Grocy.Components.ProductPicker.ShowCustomError(__t('This product is not in stock'));
Grocy.Components.ProductPicker.Focus();
}
},
function(xhr) {
console.error(xhr);
}
);
},
function(xhr) {
console.error(xhr);
}
);
});