Implemented "Scan mode"

This commit is contained in:
Bernd Bestel
2020-01-26 15:35:01 +01:00
parent 7a048136c6
commit c7bcb9984a
16 changed files with 190 additions and 22 deletions

View File

@@ -4,6 +4,14 @@
- From there you can also edit the stock entries - From there you can also edit the stock entries
- A huge THANK YOU goes to @kriddles for the work on this feature - A huge THANK YOU goes to @kriddles for the work on this feature
### New feature: Scan mode
- New switch-button on the purchase and consume page
- When enabled
- The amount will always be filled with `1` after changing/scanning a product
- If all fields could be automatically populated (means for purchase the product has a default best before date set), the transaction is automatically submitted
- If not, a warning is displayed and you can fill in the missing information
- Audio feedback is provided after scanning and on success/error of the transaction
### New feature: Self produced products ### New feature: Self produced products
- To a recipe a product can be attached - To a recipe a product can be attached
- This products needs a "Default best before date" - This products needs a "Default best before date"

View File

@@ -91,6 +91,8 @@ DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for
DefaultUserSetting('stock_expring_soon_days', 5); DefaultUserSetting('stock_expring_soon_days', 5);
DefaultUserSetting('stock_default_purchase_amount', 0); DefaultUserSetting('stock_default_purchase_amount', 0);
DefaultUserSetting('stock_default_consume_amount', 1); DefaultUserSetting('stock_default_consume_amount', 1);
DefaultUserSetting('scan_mode_consume_enabled', false);
DefaultUserSetting('scan_mode_purchase_enabled', false);
# Chores settings # Chores settings
DefaultUserSetting('chores_due_soon_days', 5); DefaultUserSetting('chores_due_soon_days', 5);

View File

@@ -1669,3 +1669,15 @@ msgstr ""
msgid "Meal plan product" msgid "Meal plan product"
msgstr "" msgstr ""
msgid "Scan mode"
msgstr ""
msgid "on"
msgstr ""
msgid "off"
msgstr ""
msgid "Scan mode is on but not all required fields could be populated automatically"
msgstr ""

View File

@@ -8,6 +8,7 @@
"bootbox": "^5.3.2", "bootbox": "^5.3.2",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"bootstrap-select": "^1.13.10", "bootstrap-select": "^1.13.10",
"bootstrap-switch-button": "https://github.com/walidbagh/bootstrap-switch-button#Fix-module-export",
"chart.js": "^2.8.0", "chart.js": "^2.8.0",
"datatables.net": "^1.10.19", "datatables.net": "^1.10.19",
"datatables.net-bs4": "^1.10.19", "datatables.net-bs4": "^1.10.19",

View File

@@ -425,7 +425,7 @@ $(document).on("click", "select", function()
}); });
// Auto saving user setting controls // Auto saving user setting controls
$(".user-setting-control").on("change", function() $(document).on("change", ".user-setting-control", function()
{ {
var element = $(this); var element = $(this);
var settingKey = element.attr("data-setting-key"); var settingKey = element.attr("data-setting-key");

View File

@@ -0,0 +1,26 @@
Grocy.UISound = { };
Grocy.UISound.Play = function(url)
{
new Audio(url).play();
}
Grocy.UISound.AskForPermission = function()
{
Grocy.UISound.Play(U("/uisounds/silence.mp3"));
}
Grocy.UISound.Success = function()
{
Grocy.UISound.Play(U("/uisounds/success.mp3"));
}
Grocy.UISound.Error = function()
{
Grocy.UISound.Play(U("/uisounds/error.mp3"));
}
Grocy.UISound.BarcodeScannerBeep = function()
{
Grocy.UISound.Play(U("/uisounds/barcodescannerbeep.mp3"));
}

Binary file not shown.

BIN
public/uisounds/error.mp3 Normal file

Binary file not shown.

BIN
public/uisounds/silence.mp3 Normal file

Binary file not shown.

BIN
public/uisounds/success.mp3 Normal file

Binary file not shown.

View File

@@ -38,6 +38,11 @@
Grocy.Api.Post(apiUrl, jsonData, Grocy.Api.Post(apiUrl, jsonData,
function(result) function(result)
{ {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{
Grocy.UISound.Success();
}
bookingResponse = result; bookingResponse = result;
var addBarcode = GetUriParam('addbarcodetoselection'); var addBarcode = GetUriParam('addbarcodetoselection');
@@ -246,6 +251,11 @@ $("#location_id").on('change', function(e)
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{ {
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{
Grocy.UISound.BarcodeScannerBeep();
}
$("#specific_stock_entry").find("option").remove().end().append("<option></option>"); $("#specific_stock_entry").find("option").remove().end().append("<option></option>");
if ($("#use_specific_stock_entry").is(":checked")) if ($("#use_specific_stock_entry").is(":checked"))
{ {
@@ -265,13 +275,14 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name); $('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
$("#location_id").find("option").remove().end().append("<option></option>"); $("#location_id").find("option").remove().end().append("<option></option>");
Grocy.Api.Get("stock/products/" + productId + '/locations', Grocy.Api.Get("stock/products/" + productId + '/locations',
function(stockLocations) function(stockLocations)
{ {
var setDefault = 0; var setDefault = 0;
stockLocations.forEach(stockLocation => stockLocations.forEach(stockLocation =>
{ {
if (productDetails.location.id == stockLocation.location_id) { if (productDetails.location.id == stockLocation.location_id)
{
$("#location_id").append($("<option>", { $("#location_id").append($("<option>", {
value: stockLocation.location_id, value: stockLocation.location_id,
text: stockLocation.location_name + " (" + __t("Default location") + ")" text: stockLocation.location_name + " (" + __t("Default location") + ")"
@@ -294,6 +305,21 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
$("#location_id").trigger('change'); $("#location_id").trigger('change');
} }
}); });
if (BoolVal(Grocy.UserSettings.scan_mode_consume_enabled))
{
$("#amount").val(1);
Grocy.FrontendHelpers.ValidateForm("consume-form");
if (document.getElementById("consume-form").checkValidity() === true)
{
$('#save-consume-button').click();
}
else
{
toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically"));
Grocy.UISound.Error();
}
}
}, },
function(xhr) function(xhr)
{ {
@@ -495,3 +521,18 @@ if (GetUriParam("embedded") !== undefined)
$("#use_specific_stock_entry").trigger('change'); $("#use_specific_stock_entry").trigger('change');
} }
} }
// Default input field
Grocy.Components.ProductPicker.GetInputElement().focus();
// Can only be set via JS however...
$("#scan-mode").addClass("user-setting-control");
$("#scan-mode").attr("data-setting-key", "scan_mode_consume_enabled");
$(document).on("change", "#scan-mode", function(e)
{
if ($(this).prop("checked"))
{
Grocy.UISound.AskForPermission();
}
});

View File

@@ -42,6 +42,11 @@
Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData, Grocy.Api.Post('stock/products/' + jsonForm.product_id + '/add', jsonData,
function(result) function(result)
{ {
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
{
Grocy.UISound.Success();
}
var addBarcode = GetUriParam('addbarcodetoselection'); var addBarcode = GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined) if (addBarcode !== undefined)
{ {
@@ -122,6 +127,11 @@ if (Grocy.Components.ProductPicker !== undefined)
{ {
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{ {
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
{
Grocy.UISound.BarcodeScannerBeep();
}
var productId = $(e.target).val(); var productId = $(e.target).val();
if (productId) if (productId)
@@ -129,7 +139,7 @@ if (Grocy.Components.ProductPicker !== undefined)
Grocy.Components.ProductCard.Refresh(productId); Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/products/' + productId, Grocy.Api.Get('stock/products/' + productId,
function (productDetails) function(productDetails)
{ {
$('#price').val(productDetails.last_price); $('#price').val(productDetails.last_price);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@@ -205,6 +215,21 @@ if (Grocy.Components.ProductPicker !== undefined)
$('#amount').focus(); $('#amount').focus();
} }
} }
if (BoolVal(Grocy.UserSettings.scan_mode_purchase_enabled))
{
$("#amount").val(1);
Grocy.FrontendHelpers.ValidateForm("purchase-form");
if (document.getElementById("purchase-form").checkValidity() === true)
{
$('#save-purchase-button').click();
}
else
{
toastr.warning(__t("Scan mode is on but not all required fields could be populated automatically"));
Grocy.UISound.Error();
}
}
}, },
function(xhr) function(xhr)
{ {
@@ -336,3 +361,15 @@ function UndoStockTransaction(transactionId)
} }
); );
}; };
// Can only be set via JS however...
$("#scan-mode").addClass("user-setting-control");
$("#scan-mode").attr("data-setting-key", "scan_mode_purchase_enabled");
$(document).on("change", "#scan-mode", function(e)
{
if ($(this).prop("checked"))
{
Grocy.UISound.AskForPermission();
}
});

View File

@@ -440,3 +440,6 @@ if (GetUriParam("embedded") !== undefined)
$("#use_specific_stock_entry").trigger('change'); $("#use_specific_stock_entry").trigger('change');
} }
} }
// Default input field
Grocy.Components.ProductPicker.GetInputElement().focus();

View File

@@ -4,10 +4,22 @@
@section('activeNav', 'consume') @section('activeNav', 'consume')
@section('viewJsName', 'consume') @section('viewJsName', 'consume')
@push('pageStyles')
<link href="{{ $U('/node_modules/bootstrap-switch-button/css/bootstrap-switch-button.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@push('pageScripts')
<script src="{{ $U('/node_modules/bootstrap-switch-button/js/bootstrap-switch-button.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy_uisound.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-6 col-xl-4 pb-3"> <div class="col-xs-12 col-md-6 col-xl-4 pb-3">
<h1>@yield('title')</h1> <h1>
@yield('title')
<input @if(boolval($userSettings['scan_mode_consume_enabled'])) checked @endif id="scan-mode" type="checkbox" data-setting-key="scan_mode_consume_enabled" data-toggle="switchbutton" data-onlabel="{{ $__t('Scan mode') }} {{ $__t('on') }}" data-offlabel="{{ $__t('Scan mode') }} {{ $__t('off') }}" data-onstyle="success" data-offstyle="primary" data-style="ml-2" data-width="160">
</h1>
<form id="consume-form" novalidate> <form id="consume-form" novalidate>
@@ -28,23 +40,23 @@
)) ))
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
@php /*@include('components.locationpicker', array( @php /*@include('components.locationpicker', array(
'id' => 'location_id', 'id' => 'location_id',
'locations' => $locations, 'locations' => $locations,
'isRequired' => true, 'isRequired' => true,
'label' => 'Location' 'label' => 'Location'
))*/ @endphp ))*/ @endphp
<div class="form-group"> <div class="form-group">
<label for="location_id">{{ $__t('Location') }}</label> <label for="location_id">{{ $__t('Location') }}</label>
<select required class="form-control location-combobox" id="location_id" name="location_id"> <select required class="form-control location-combobox" id="location_id" name="location_id">
<option></option> <option></option>
@foreach($locations as $location) @foreach($locations as $location)
<option value="{{ $location->id }}">{{ $location->name }}</option> <option value="{{ $location->id }}">{{ $location->name }}</option>
@endforeach @endforeach
</select> </select>
<div class="invalid-feedback">{{ $__t('A location is required') }}</div> <div class="invalid-feedback">{{ $__t('A location is required') }}</div>
</div> </div>
@else @else
<input type="hidden" name="location_id" id="location_id" value="1"> <input type="hidden" name="location_id" id="location_id" value="1">
@endif @endif

View File

@@ -4,10 +4,22 @@
@section('activeNav', 'purchase') @section('activeNav', 'purchase')
@section('viewJsName', 'purchase') @section('viewJsName', 'purchase')
@push('pageStyles')
<link href="{{ $U('/node_modules/bootstrap-switch-button/css/bootstrap-switch-button.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@push('pageScripts')
<script src="{{ $U('/node_modules/bootstrap-switch-button/js/bootstrap-switch-button.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy_uisound.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content') @section('content')
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-6 col-xl-4 pb-3"> <div class="col-xs-12 col-md-6 col-xl-4 pb-3">
<h1>@yield('title')</h1> <h1>
@yield('title')
<input @if(boolval($userSettings['scan_mode_purchase_enabled'])) checked @endif id="scan-mode" type="checkbox" data-setting-key="scan_mode_purchase_enabled" data-toggle="switchbutton" data-onlabel="{{ $__t('Scan mode') }} {{ $__t('on') }}" data-offlabel="{{ $__t('Scan mode') }} {{ $__t('off') }}" data-onstyle="success" data-offstyle="primary" data-style="ml-2" data-width="160">
</h1>
<form id="purchase-form" novalidate> <form id="purchase-form" novalidate>

View File

@@ -17,6 +17,11 @@
dependencies: dependencies:
jquery "1" jquery "1"
add@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235"
integrity sha1-JI8Kn25aUo7yKV2+7DBTITCuIjU=
ajv@^6.5.5: ajv@^6.5.5:
version "6.10.2" version "6.10.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
@@ -75,6 +80,10 @@ bootstrap-select@^1.13.10:
resolved "https://registry.yarnpkg.com/bootstrap-select/-/bootstrap-select-1.13.11.tgz#d364ccc67cd6c1f9158689087f1d2d029df82c29" resolved "https://registry.yarnpkg.com/bootstrap-select/-/bootstrap-select-1.13.11.tgz#d364ccc67cd6c1f9158689087f1d2d029df82c29"
integrity sha512-WPpx2DYL9jVilNoqy4Pjcfa/Q0LOq8V0+xws/pmnRDn/deS7OYjo1njvD1Cv0s9/1ZUXt77UypxuloimEnkYsA== integrity sha512-WPpx2DYL9jVilNoqy4Pjcfa/Q0LOq8V0+xws/pmnRDn/deS7OYjo1njvD1Cv0s9/1ZUXt77UypxuloimEnkYsA==
"bootstrap-switch-button@https://github.com/walidbagh/bootstrap-switch-button#Fix-module-export":
version "1.0.0"
resolved "https://github.com/walidbagh/bootstrap-switch-button#551ccd361cc9e291cd04c2278357b1db76318f1e"
bootstrap@4.0.0: bootstrap@4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
@@ -779,3 +788,8 @@ verror@1.10.0:
assert-plus "^1.0.0" assert-plus "^1.0.0"
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
yarn@^1.21.1:
version "1.21.1"
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.21.1.tgz#1d5da01a9a03492dc4a5957befc1fd12da83d89c"
integrity sha512-dQgmJv676X/NQczpbiDtc2hsE/pppGDJAzwlRiADMTvFzYbdxPj2WO4PcNyriSt2c4jsCMpt8UFRKHUozt21GQ==