Modernize Javascript

This *absolute commit monster* does the following things:

- Introduce gulp to build javascript and css files.

  This includes moving node_modules out of the public/ folder.
  Use `gulp --tasks` to get a list of all tasks; however some
  of them are automatically generated.

  Use `gulp live` to watch for changes and automatically recompile
  what's needed.

- Upgrade to yarn2
- Upgrade FullCalendar to 4.4.2

  I know that 5.x is the current version, but two major version upgrades
  are too much right now. Also v5 would break any custom css as they
  renamed a bunch of classes.

- Move Styles to sass

  (Most) global styles are now included in one sass file. This also
  means that we now compile our own bootstrap.

- Javascript is now in strict mode

  Because everything is a module now, `use strict` is now in effect
  for all javascript files. There are probably still some parts left
  where implicit variable declarations were used.

- grocy*.js were split up in modules.

  `window.Grocy` is now an instance of GrocyClass. API-wise nothing
  has changed (albeit some functions were added regarding Undo actions)
  At the Moment, this leaks a whole bunch of functions into window
  (that was easier than tracking those down).

- FindObjectIn... style functions were removed.

  Array.prototype.find and Array.prototype.filter suffice.

- Use babel to preprocess javascript.
- Use rollup to bundle javascript.

  rollup bundles and tree-shakes es6 javascript bundles.
  It also allows to "import" css files and generate css
  files specific to this javascript file. This is used
  in viewjs scripts, for example when importing FullCalendar,
  to generate an associated viewcss file.

- Use postcss to post-process css files.

  postcss uses autoprefixer to handle browser compatiblity.
  Internally this uses the package `browserslist`; and is currently
  configured to the default setting.

- Minify assets when building in production

  `gulp publish` builds all assets in production mode, that is,
  the assets get minified. This includes javascript as well as
  css files.

- css bundling

  concatCss is used to pull @imports of non-sass-files into one
  grocy.css

- animate.css is now in the main bundle

  animate.css was used in so many places that it is now located
  in the main style bundle.
This commit is contained in:
Katharina Bogad 2021-06-18 12:44:39 +02:00
parent fe59fac1c3
commit f7bc6a3f6d
175 changed files with 13579 additions and 5860 deletions

5
.babelrc Normal file
View File

@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
/public/node_modules
node_modules
.yarn
/vendor
/.release
embedded.txt
embedded.txt

4
.yarnrc.yml Normal file
View File

@ -0,0 +1,4 @@
yarnPath: ".yarn/releases/yarn-berry.cjs"
nodeLinker: node-modules
# we explicitly do not set the old files!
# js, css delivery and bundling will be done by webpack.

View File

@ -76,6 +76,7 @@ class SystemApiController extends BaseApiController
$requestBody = $this->GetParsedAndFilteredRequestBody($request);
$this->getLocalizationService()->CheckAndAddMissingTranslationToPot($requestBody['text']);
file_put_contents("php://stderr", print_r($requestBody['text'], true));
return $this->EmptyApiResponse($response);
}
catch (\Exception $ex)

193
gulpfile.babel.js Normal file
View File

@ -0,0 +1,193 @@
'use strict';
import { series, parallel, dest, src, watch, task } from 'gulp';
import rollup from '@rollup/stream';
import sourcemaps from 'gulp-sourcemaps';
import source from 'vinyl-source-stream';
import buffer from 'vinyl-buffer';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import rollupCss from 'rollup-plugin-css-porter';
import gulpif from 'gulp-if';
import uglify from 'gulp-uglify';
import gulpsass from 'gulp-dart-sass'; // TODO: move to gulp-sass once they removed the node-sass depenency
import postcss from 'gulp-postcss';
import glob from 'glob';
import path from 'path';
// css post-processing
import cssnano from 'cssnano';
import autoprefixer from 'autoprefixer';
import concatCss from 'gulp-concat-css';
var minify = false;
var postcss_plugins = [
// always add autoprefixer
autoprefixer(),
];
// viewjs handling
var files = glob.sync('./js/viewjs/*.js');
var components = glob.sync('./js/viewjs/components/*.js');
var viewJStasks = [];
files.forEach(function(target)
{
task(target, cb => rollup({
input: target,
output: {
format: 'umd',
name: path.basename(target),
sourcemap: 'inline',
},
plugins: [resolve(), rollupCss({
dest: './public/css/viewcss/' + path.basename(target).replace(".js", ".css")
}), commonjs()],
})
.pipe(source(path.basename(target), "./js/viewjs"))
.pipe(gulpif(minify, uglify()))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write('.'))
.pipe(dest('./public/viewjs')));
viewJStasks.push(target);
});
components.forEach(function(target)
{
task(target, cb => rollup({
input: target,
output: {
format: 'umd',
name: path.basename(target),
sourcemap: 'inline',
},
plugins: [resolve(), rollupCss({
dest: './public/css/components/' + path.basename(target).replace(".js", ".css")
}), commonjs()],
})
.pipe(source(path.basename(target), "./js/viewjs/components"))
.pipe(gulpif(minify, uglify()))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write('.'))
.pipe(dest('./public/viewjs/components')));
viewJStasks.push(target);
});
// The `clean` function is not exported so it can be considered a private task.
// It can still be used within the `series()` composition.
function clean(cb)
{
// body omitted
cb();
}
// The `build` function is exported so it is public and can be run with the `gulp` command.
// It can also be used within the `series()` composition.
function build(cb)
{
// body omitted
return parallel(js, css, vendor, resourceFileCopy);
}
function publish(cb)
{
minify = true;
postcss_plugins.push(cssnano())
return build();
}
function js(cb)
{
return rollup({
input: './js/grocy.js',
output: {
format: 'umd',
name: 'grocy.js',
sourcemap: 'inline',
},
plugins: [resolve(), commonjs()],
})
.pipe(source('grocy.js', "./js"))
.pipe(gulpif(minify, uglify()))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write('.'))
.pipe(dest('./public/js'));
}
function viewjs(cb)
{
return parallel(viewJStasks, done => { done(); cb(); })();
}
function vendor(cb)
{
return rollup({
input: './js/vendor.js',
output: {
format: 'umd',
name: 'grocy.js',
sourcemap: 'inline',
},
plugins: [resolve(), commonjs()],
})
.pipe(source('vendor.js', "./js"))
.pipe(gulpif(minify, uglify()))
.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(sourcemaps.write('.'))
.pipe(dest('./public/js'));
}
function css(cb)
{
return src('./scss/grocy.scss')
.pipe(sourcemaps.init())
.pipe(gulpsass({ includePaths: ['./node_modules'], quietDeps: true }).on('error', gulpsass.logError))
.pipe(concatCss('grocy.css', { includePaths: ['./node_modules'], rebaseUrls: false }))
.pipe(postcss(postcss_plugins))
.pipe(sourcemaps.write('.'))
.pipe(dest('./public/css'))
}
function resourceFileCopy(cb)
{
return parallel(
cb => src([
'./node_modules/@fortawesome/fontawesome-free/webfonts/*'
]).pipe(dest('./public/webfonts')),
cb => src('./node_modules/summernote/dist/font/*').pipe(dest('./public/css/font')),
done => { done(); cb(); }
)();
}
function copyLocales(cb)
{
return parallel(
cb => src('./node_modules/timeago/locales/*.js').pipe(dest('./public/js/locales/timeago')),
cb => src('./node_modules/summernote/dist/lang/*.js').pipe(dest('./public/js/locales/summernote')),
cb => src('./node_modules/bootstrap-select/dist/js/i18n/*').pipe(dest('./public/js/locales/bootstrap-select')),
cb => src('./node_modules/fullcalendar/dist/locale/*').pipe(dest('./public/js/locales/fullcalendar')),
cb => src('./node_modules/@fullcalendar/core/locales/*').pipe(dest('./public/js/locales/fullcalendar-core')),
done => { done(); cb(); }
)();
}
function live(cb)
{
watch('./scss/**/*.scss', css);
watch(['./js/**/*.js', '!!./js/viewjs/**/*.js'], js);
watch('./js/vendor.js', vendor);
//watch('./js/viewjs/**/*.js', viewjs);
viewJStasks.forEach(elem => watch(elem, series([elem])));
}
export { build, js, vendor, viewjs, css, live, clean, resourceFileCopy, copyLocales }

113
js/configs/datatable.js Normal file
View File

@ -0,0 +1,113 @@
import { IsJsonString } from '../helpers/extensions';
function setDatatableDefaults(Grocy)
{
// Default DataTables initialisation settings
var collapsedGroups = {};
$.extend(true, $.fn.dataTable.defaults, {
'paginate': false,
'deferRender': true,
'language': IsJsonString(__t('datatables_localization')) ? JSON.parse(__t('datatables_localization')) : {},
'scrollY': false,
'scrollX': true,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
},
'stateSaveCallback': function(settings, data)
{
var settingKey = 'datatables_state_' + settings.sTableId;
if ($.isEmptyObject(data))
{
//state.clear was called and unfortunately the table is not refresh, so we are reloading the page
Grocy.FrontendHelpers.DeleteUserSetting(settingKey, true);
} else
{
var stateData = JSON.stringify(data);
Grocy.FrontendHelpers.SaveUserSetting(settingKey, stateData);
}
},
'stateLoadCallback': function(settings, data)
{
var settingKey = 'datatables_state_' + settings.sTableId;
if (Grocy.UserSettings[settingKey] == undefined)
{
return null;
}
else
{
return JSON.parse(Grocy.UserSettings[settingKey]);
}
},
'preDrawCallback': function(settings)
{
// Currently it is not possible to save the state of rowGroup via saveState events
var api = new $.fn.dataTable.Api(settings);
if (typeof api.rowGroup === "function")
{
var settingKey = 'datatables_rowGroup_' + settings.sTableId;
if (Grocy.UserSettings[settingKey] !== undefined)
{
var rowGroup = JSON.parse(Grocy.UserSettings[settingKey]);
// Check if there way changed. the draw event is called often therefore we have to check if it's really necessary
if (rowGroup.enable !== api.rowGroup().enabled()
|| ("dataSrc" in rowGroup && rowGroup.dataSrc !== api.rowGroup().dataSrc()))
{
api.rowGroup().enable(rowGroup.enable);
if ("dataSrc" in rowGroup)
{
api.rowGroup().dataSrc(rowGroup.dataSrc);
// Apply fixed order for group column
var fixedOrder = {
pre: [rowGroup.dataSrc, 'asc']
};
api.order.fixed(fixedOrder);
}
}
}
}
},
'columnDefs': [
{ type: 'chinese-string', targets: '_all' }
],
'rowGroup': {
enable: false,
startRender: function(rows, group)
{
var collapsed = !!collapsedGroups[group];
var toggleClass = collapsed ? "fa-caret-right" : "fa-caret-down";
rows.nodes().each(function(row)
{
row.style.display = collapsed ? "none" : "";
});
return $("<tr/>")
.append('<td colspan="' + rows.columns()[0].length + '">' + group + ' <span class="fa fa-fw d-print-none ' + toggleClass + '"/></td>')
.attr("data-name", group)
.toggleClass("collapsed", collapsed);
}
}
});
$(document).on("click", "tr.dtrg-group", function()
{
var name = $(this).data('name');
collapsedGroups[name] = !collapsedGroups[name];
$("table").DataTable().draw();
});
}
export { setDatatableDefaults }

497
js/configs/globalstate.js Normal file
View File

@ -0,0 +1,497 @@
import { ResizeResponsiveEmbeds } from "../helpers/embeds";
import { IsTouchInputDevice } from "../helpers/input";
import { BoolVal } from "../helpers/extensions";
// This function sets some global state and adds some global event listeners.
function setInitialGlobalState(Grocy)
{
// __t isn't set in global context yet, make one locally.
var __t = function(key, ...placeholder) { return Grocy.translate(key, ...placeholder) };
if (!Grocy.ActiveNav.isEmpty())
{
var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']");
menuItem.addClass('active-page');
if (menuItem.length)
{
var parentMenuSelector = menuItem.data("sub-menu-of");
if (typeof parentMenuSelector !== "undefined")
{
$(parentMenuSelector).collapse("show");
$(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page");
$(parentMenuSelector).on("shown.bs.collapse", function(e)
{
if (!menuItem.isVisibleInViewport(75))
{
menuItem[0].scrollIntoView();
}
})
}
else
{
if (!menuItem.isVisibleInViewport(75))
{
menuItem[0].scrollIntoView();
}
}
}
}
var observer = new MutationObserver(function(mutations)
{
mutations.forEach(function(mutation)
{
if (mutation.attributeName === "class")
{
var attributeValue = $(mutation.target).prop(mutation.attributeName);
if (attributeValue.contains("sidenav-toggled"))
{
window.localStorage.setItem("sidebar_state", "collapsed");
}
else
{
window.localStorage.setItem("sidebar_state", "expanded");
}
}
});
});
observer.observe(document.body, {
attributes: true
});
if (window.localStorage.getItem("sidebar_state") === "collapsed")
{
$("#sidenavToggler").click();
}
window.toastr.options = {
toastClass: 'alert',
closeButton: true,
timeOut: 20000,
extendedTimeOut: 5000
};
window.FontAwesomeConfig = {
searchPseudoElements: true
}
$(window).on('resize', function()
{
ResizeResponsiveEmbeds($("body").hasClass("fullscreen-card"));
});
$("iframe").on("load", function()
{
ResizeResponsiveEmbeds($("body").hasClass("fullscreen-card"));
});
// Don't show tooltips on touch input devices
if (IsTouchInputDevice())
{
var css = document.createElement("style");
css.innerHTML = ".tooltip { display: none; }";
document.body.appendChild(css);
}
$(document).on("keyup paste change", "input, textarea", function()
{
$(this).closest("form").addClass("is-dirty");
});
$(document).on("click", "select", function()
{
$(this).closest("form").addClass("is-dirty");
});
// Auto saving user setting controls
$(document).on("change", ".user-setting-control", function()
{
var element = $(this);
var settingKey = element.attr("data-setting-key");
if (!element[0].checkValidity())
{
return;
}
var inputType = "unknown";
if (typeof element.attr("type") !== typeof undefined && element.attr("type") !== false)
{
inputType = element.attr("type").toLowerCase();
}
if (inputType === "checkbox")
{
value = element.is(":checked");
}
else
{
var value = element.val();
}
Grocy.FrontendHelpers.SaveUserSetting(settingKey, value);
});
// Show file name Bootstrap custom file input
$('input.custom-file-input').on('change', function()
{
$(this).next('.custom-file-label').html(GetFileNameFromPath($(this).val()));
});
// Translation of "Browse"-button of Bootstrap custom file input
if ($(".custom-file-label").length > 0)
{
$("<style>").html('.custom-file-label::after { content: "' + __t("Select file") + '"; }').appendTo("head");
}
// Add border around anchor link section
if (window.location.hash)
{
$(window.location.hash).addClass("p-2 border border-info rounded");
}
$("#about-dialog-link").on("click", function()
{
bootbox.alert({
message: '<iframe height="400px" class="embed-responsive" src="' + U("/about?embedded") + '"></iframe>',
closeButton: false,
size: "large"
});
});
$(document).on("click", ".easy-link-copy-textbox", function()
{
$(this).select();
});
$("textarea.wysiwyg-editor").summernote({
minHeight: "300px",
lang: __t("summernote_locale")
});
$(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();
}
});
$(document).on("click", ".show-as-dialog-link", function(e)
{
e.preventDefault();
var link = $(e.currentTarget).attr("href");
bootbox.dialog({
message: '<iframe height="650px" class="embed-responsive" src="' + link + '"></iframe>',
size: 'large',
backdrop: true,
closeButton: false,
buttons: {
cancel: {
label: __t('Close'),
className: 'btn-secondary responsive-button',
callback: function()
{
bootbox.hideAll();
}
}
}
});
});
// serializeJSON defaults
$.serializeJSON.defaultOptions.checkboxUncheckedValue = "0";
$('a.link-return').not(".btn").each(function()
{
var base = $(this).data('href');
if (base.contains('?'))
{
$(this).attr('href', base + '&returnto' + encodeURIComponent(location.pathname));
}
else
{
$(this).attr('href', base + '?returnto=' + encodeURIComponent(location.pathname));
}
})
$(document).on("click", "a.btn.link-return", function(e)
{
e.preventDefault();
var link = GetUriParam("returnto");
if (!link || !link.length > 0)
{
location.href = $(e.currentTarget).attr("href");
}
else
{
location.href = U(link);
}
});
$('.dropdown-item').has('.form-check input[type=checkbox]').on('click', function(e)
{
if ($(e.target).is('div.form-check') || $(e.target).is('div.dropdown-item'))
{
$(e.target).find('input[type=checkbox]').click();
}
})
$('.table').on('column-sizing.dt', function(e, settings)
{
var dtScrollWidth = $('.dataTables_scroll').width();
var tableWidth = $('.table').width();
if (dtScrollWidth < tableWidth)
{
$('.dataTables_scrollBody').addClass("no-force-overflow-visible");
$('.dataTables_scrollBody').removeClass("force-overflow-visible");
}
else
{
$('.dataTables_scrollBody').removeClass("no-force-overflow-visible");
$('.dataTables_scrollBody').addClass("force-overflow-visible");
}
});
$('td .dropdown').on('show.bs.dropdown', function(e)
{
if ($('.dataTables_scrollBody').hasClass("no-force-overflow-visible"))
{
$('.dataTables_scrollBody').addClass("force-overflow-visible");
}
});
$("td .dropdown").on('hide.bs.dropdown', function(e)
{
if ($('.dataTables_scrollBody').hasClass("no-force-overflow-visible"))
{
$('.dataTables_scrollBody').removeClass("force-overflow-visible");
}
})
$(window).on("message", function(e)
{
var data = e.originalEvent.data;
if (data.Message === "Reload")
{
window.location.reload();
}
});
$(".change-table-columns-visibility-button").on("click", function(e)
{
e.preventDefault();
var dataTableSelector = $(e.currentTarget).attr("data-table-selector");
var dataTable = $(dataTableSelector).DataTable();
var columnCheckBoxesHtml = "";
var rowGroupRadioBoxesHtml = "";
var rowGroupDefined = typeof dataTable.rowGroup === "function";
if (rowGroupDefined)
{
var rowGroupChecked = (dataTable.rowGroup().enabled()) ? "" : "checked";
rowGroupRadioBoxesHtml = ' \
<div class="custom-control custom-radio custom-control-inline"> \
<input ' + rowGroupChecked + ' class="custom-control-input change-table-columns-rowgroup-toggle" \
type="radio" \
name="column-rowgroup" \
id="column-rowgroup-none" \
data-table-selector="' + dataTableSelector + '" \
data-column-index="-1" \
> \
<label class="custom-control-label font-italic" \
for="column-rowgroup-none">' + __t("None") + ' \
</label > \
</div>';
}
dataTable.columns().every(function()
{
var index = this.index();
var title = $(this.header()).text();
var visible = this.visible();
if (title.isEmpty() || title.startsWith("Hidden"))
{
return;
}
var checked = "checked";
if (!visible)
{
checked = "";
}
columnCheckBoxesHtml += ' \
<div class="custom-control custom-checkbox"> \
<input ' + checked + ' class="form-check-input custom-control-input change-table-columns-visibility-toggle" \
type="checkbox" \
id="column-' + index.toString() + '" \
data-table-selector="' + dataTableSelector + '" \
data-column-index="' + index.toString() + '" \
value="1"> \
<label class="form-check-label custom-control-label" \
for="column-' + index.toString() + '">' + title + ' \
</label> \
</div>';
if (rowGroupDefined)
{
var rowGroupChecked = "";
if (dataTable.rowGroup().enabled() && dataTable.rowGroup().dataSrc() == index)
{
rowGroupChecked = "checked";
}
rowGroupRadioBoxesHtml += ' \
<div class="custom-control custom-radio"> \
<input ' + rowGroupChecked + ' class="custom-control-input change-table-columns-rowgroup-toggle" \
type="radio" \
name="column-rowgroup" \
id="column-rowgroup-' + index.toString() + '" \
data-table-selector="' + dataTableSelector + '" \
data-column-index="' + index.toString() + '" \
> \
<label class="custom-control-label" \
for="column-rowgroup-' + index.toString() + '">' + title + ' \
</label > \
</div>';
}
});
var message = '<div class="text-center"><h5>' + __t('Table options') + '</h5><hr><h5 class="mb-0">' + __t('Hide/view columns') + '</h5><div class="text-left form-group">' + columnCheckBoxesHtml + '</div></div>';
if (rowGroupDefined)
{
message += '<div class="text-center mt-1"><h5 class="pt-3 mb-0">' + __t('Group by') + '</h5><div class="text-left form-group">' + rowGroupRadioBoxesHtml + '</div></div>';
}
bootbox.dialog({
message: message,
size: 'small',
backdrop: true,
closeButton: false,
buttons: {
reset: {
label: __t('Reset'),
className: 'btn-outline-danger float-left responsive-button',
callback: function()
{
bootbox.confirm({
message: __t("Are you sure to reset the table options?"),
buttons: {
cancel: {
label: 'No',
className: 'btn-danger'
},
confirm: {
label: 'Yes',
className: 'btn-success'
}
},
callback: function(result)
{
if (result)
{
var dataTable = $(dataTableSelector).DataTable();
var tableId = dataTable.settings()[0].sTableId;
// Delete rowgroup settings
Grocy.FrontendHelpers.DeleteUserSetting('datatables_rowGroup_' + tableId);
// Delete state settings
dataTable.state.clear();
}
bootbox.hideAll();
}
});
}
},
ok: {
label: __t('OK'),
className: 'btn-primary responsive-button',
callback: function()
{
bootbox.hideAll();
}
}
}
});
});
$(document).on("click", ".change-table-columns-visibility-toggle", function()
{
var dataTableSelector = $(this).attr("data-table-selector");
var columnIndex = $(this).attr("data-column-index");
var dataTable = $(dataTableSelector).DataTable();
dataTable.columns(columnIndex).visible(this.checked);
});
$(document).on("click", ".change-table-columns-rowgroup-toggle", function()
{
var dataTableSelector = $(this).attr("data-table-selector");
var columnIndex = $(this).attr("data-column-index");
var dataTable = $(dataTableSelector).DataTable();
var rowGroup;
if (columnIndex == -1)
{
rowGroup = {
enable: false
};
dataTable.rowGroup().enable(false);
// Remove fixed order
dataTable.order.fixed({});
}
else
{
rowGroup = {
enable: true,
dataSrc: columnIndex
}
dataTable.rowGroup().enable(true);
dataTable.rowGroup().dataSrc(columnIndex);
// Apply fixed order for group column
var fixedOrder = {
pre: [columnIndex, 'asc']
};
dataTable.order.fixed(fixedOrder);
}
var settingKey = 'datatables_rowGroup_' + dataTable.settings()[0].sTableId;
Grocy.FrontendHelpers.SaveUserSetting(settingKey, JSON.stringify(rowGroup));
dataTable.draw();
});
$(document).on("change", "#show-clock-in-header", function()
{
CheckHeaderClockEnabled();
});
if (Grocy.UserId !== -1 && BoolVal(Grocy.UserSettings.auto_reload_on_db_change))
{
$("#auto-reload-enabled").prop("checked", true);
}
}
export { setInitialGlobalState }

9
js/configs/lazy.js Normal file
View File

@ -0,0 +1,9 @@
function LoadImagesLazy()
{
$(".lazy").Lazy({
enableThrottle: true,
throttle: 500
});
}
export { LoadImagesLazy }

11
js/configs/permissions.js Normal file
View File

@ -0,0 +1,11 @@
function setPermissions(permissions)
{
for (let item of permissions)
{
if (item.has_permission == 0)
{
$('.permission-' + item.permission_name).addClass('disabled').addClass('not-allowed');
}
}
}

48
js/configs/timeago.js Normal file
View File

@ -0,0 +1,48 @@
function RefreshContextualTimeago(rootSelector = "#page-content")
{
$.timeago.settings.allowFuture = true;
$(rootSelector + " time.timeago").each(function()
{
var element = $(this);
if (!element.hasAttr("datetime"))
{
element.text("")
return
}
var timestamp = element.attr("datetime");
if (timestamp.isEmpty())
{
element.text("")
return
}
var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31";
var isToday = timestamp && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD");
var isDateWithoutTime = element.hasClass("timeago-date-only");
if (isNever)
{
element.prev().text(__t("Never"));
element.text("");
}
else if (isToday)
{
element.text(__t("Today"));
}
else
{
element.timeago("update", timestamp);
}
if (isDateWithoutTime)
{
element.prev().text(element.prev().text().substring(0, 10));
}
});
}
export { RefreshContextualTimeago };

270
js/grocy.js Normal file
View File

@ -0,0 +1,270 @@
import { GrocyApi } from './lib/api';
import { RefreshContextualTimeago } from "./configs/timeago";
import { LoadImagesLazy } from "./configs/lazy";
import { setDatatableDefaults } from "./configs/datatable";
import { GrocyFrontendHelpers } from "./helpers/frontend";
import { setInitialGlobalState } from "./configs/globalstate";
import { RefreshLocaleNumberDisplay, RefreshLocaleNumberInput } from "./helpers/numberdisplay";
import { WakeLock } from "./lib/WakeLock";
import { UISound } from "./lib/UISound";
import { Nightmode } from "./lib/nightmode";
import { HeaderClock } from "./helpers/clock";
import { animateCSS, BoolVal, Delay, EmptyElementWhenMatches, GetUriParam, RemoveUriParam, UpdateUriParam } from "./helpers/extensions";
import Translator from "gettext-translator";
import "./helpers/string";
class GrocyClass
{
constructor(config)
{
// set up properties from config
this.UserSettings = config.UserSettings;
this.Mode = config.Mode;
this.UserId = config.UserId;
this.ActiveNav = config.ActiveNav;
this.CalendarFirstDayOfWeek = config.CalendarFirstDayOfWeek;
this.DatabaseChangedTime = null;
this.IdleTime = 0;
this.BaseUrl = config.BaseUrl;
this.Currency = config.Currency;
this.FeatureFlags = config.FeatureFlags;
this.QuantityUnits = config.QuantityUnits;
this.QuantityUnitConversionsResolved = config.QuantityUnitConversionsResolved || [];
this.MealPlanFirstDayOfWeek = config.MealPlanFirstDayOfWeek;
this.EditMode = config.EditMode;
this.EditObjectId = config.EditObjectId;
this.DefaultMinAmount = config.DefaultMinAmount;
this.UserPictureFileName = config.UserPictureFileName;
this.EditObjectParentId = config.EditObjectParentId;
this.EditObjectParentName = config.EditObjectParentName;
this.EditObject = config.EditObject;
this.EditObjectProduct = config.EditObjectProduct;
this.RecipePictureFileName = config.RecipePictureFileName;
this.InstructionManualFileNameName = config.InstructionManualFileNameName;
this.Components = {};
// Init some classes
this.Api = new GrocyApi(this);
this.Translator = new Translator(config.GettextPo);
this.FrontendHelpers = new GrocyFrontendHelpers(this.Api);
this.WakeLock = new WakeLock(this);
this.UISound = new UISound(this);
this.Nightmode = new Nightmode(this);
this.Nightmode.StartWatchdog();
this.HeaderClock = new HeaderClock(this);
// save the config
this.config = config;
if (!this.CalendarFirstDayOfWeek.isEmpty())
{
moment.updateLocale(moment.locale(), {
week: {
dow: Grocy.CalendarFirstDayOfWeek
}
});
}
// DB Changed Handling
if (this.UserId !== -1)
{
var self = this;
this.Api.Get('system/db-changed-time',
function(result)
{
self.DatabaseChangedTime = moment(result.changed_time);
},
function(xhr)
{
console.error(xhr);
}
);
}
}
static createSingleton(config)
{
if (window.Grocy === undefined)
{
var grocy = new GrocyClass(config);
window.Grocy = grocy;
// Check if the database has changed once a minute
setInterval(grocy.CheckDatabase(), 60000);
// Increase the idle time once every second
// On any interaction it will be reset to 0 (see above)
setInterval(grocy.IncrementIdleTime(), 1000);
window.onmousemove = grocy.ResetIdleTime;
window.onmousedown = grocy.ResetIdleTime;
window.onclick = grocy.ResetIdleTime;
window.onscroll = grocy.ResetIdleTime;
window.onkeypress = grocy.ResetIdleTime;
window.__t = function(key, ...placeholder) { return grocy.translate(key, ...placeholder) };;
window.__n = function(key, ...placeholder) { return grocy.translaten(key, ...placeholder) };
window.U = path => grocy.FormatUrl(path);
setInitialGlobalState(grocy);
RefreshContextualTimeago();
window.RefreshContextualTimeago = RefreshContextualTimeago;
RefreshLocaleNumberDisplay();
window.RefreshLocaleNumberDisplay = RefreshLocaleNumberDisplay;
RefreshLocaleNumberInput();
window.RefreshLocaleNumberInput = RefreshLocaleNumberInput;
LoadImagesLazy();
window.LoadImagesLazy = LoadImagesLazy;
setDatatableDefaults(grocy);
// add some more functions to the global space
window.Delay = Delay;
window.GetUriParam = GetUriParam;
window.UpdateUriParam = UpdateUriParam;
window.RemoveUriParam = RemoveUriParam;
window.EmptyElementWhenMatches = EmptyElementWhenMatches;
window.animateCSS = animateCSS;
}
return window.Grocy;
}
translate(text, ...placeholderValues)
{
if (this.Mode === "dev")
{
var text2 = text;
this.Api.Post('system/log-missing-localization', { "text": text2 });
}
return this.Translator.__(text, ...placeholderValues)
}
translaten(number, singularForm, pluralForm)
{
if (this.Mode === "dev")
{
var singularForm2 = singularForm;
this.Api.Post('system/log-missing-localization', { "text": singularForm2 });
}
return this.Translator.n__(singularForm, pluralForm, number, number)
}
FormatUrl(relativePath)
{
return this.BaseUrl.replace(/\/$/, '') + relativePath;
}
// If a change is detected, reload the current page, but only if already idling for at least 50 seconds,
// when there is no unsaved form data and when the user enabled auto reloading
CheckDatabase()
{
var self = this;
this.Api.Get('system/db-changed-time',
function(result)
{
var newDbChangedTime = moment(result.changed_time);
if (newDbChangedTime.isAfter(self.DatabaseChangedTime))
{
if (self.IdleTime >= 50)
{
if (BoolVal(self.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card"))
{
window.location.reload();
}
}
self.DatabaseChangedTime = newDbChangedTime;
}
},
function(xhr)
{
console.error(xhr);
}
);
}
ResetIdleTime()
{
this.IdleTime = 0;
}
IncrementIdleTime()
{
this.IdleTime += 1;
}
UndoStockBooking(bookingId)
{
this.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
}
UndoStockTransaction(transactionId)
{
this.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Transaction successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
}
UndoChoreExecution(executionId)
{
this.Api.Post('chores/executions/' + executionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Chore execution successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
}
UndoChargeCycle(chargeCycleId)
{
this.Api.Post('batteries/charge-cycles/' + chargeCycleId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Charge cycle successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
}
UndoStockBookingEntry(bookingId, stockRowId)
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result)
{
window.postMessage(WindowMessageBag("StockEntryChanged", stockRowId), Grocy.BaseUrl);
toastr.success(__t("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
}
// also set on the Window object, just because.
window.GrocyClass = GrocyClass;
export default GrocyClass;

51
js/helpers/clock.js Normal file
View File

@ -0,0 +1,51 @@
import { BoolVal } from './extensions'
class HeaderClock
{
constructor(Grocy)
{
this.Grocy = Grocy;
this.HeaderClockInterval = null;
this.CheckHeaderClockEnabled();
if (this.Grocy.UserId !== -1 && BoolVal(this.Grocy.UserSettings.show_clock_in_header))
{
$("#show-clock-in-header").prop("checked", true);
}
}
RefreshHeaderClock()
{
$("#clock-small").text(moment().format("l LT"));
$("#clock-big").text(moment().format("LLLL"));
}
CheckHeaderClockEnabled()
{
if (this.Grocy.UserId === -1)
{
return;
}
// Refresh the clock in the header every second when enabled
if (BoolVal(this.Grocy.UserSettings.show_clock_in_header))
{
RefreshHeaderClock();
$("#clock-container").removeClass("d-none");
this.HeaderClockInterval = setInterval(this.RefreshHeaderClock, 1000);
}
else
{
if (this.HeaderClockInterval !== null)
{
clearInterval(this.HeaderClockInterval);
this.HeaderClockInterval = null;
}
$("#clock-container").addClass("d-none");
}
}
}
export { HeaderClock }

20
js/helpers/embeds.js Normal file
View File

@ -0,0 +1,20 @@
function ResizeResponsiveEmbeds(fillEntireViewport = false)
{
if (!fillEntireViewport)
{
var maxHeight = $("body").height() - $("#mainNav").outerHeight() - 62;
}
else
{
var maxHeight = $("body").height();
}
$("embed.embed-responsive").attr("height", maxHeight.toString() + "px");
$("iframe.embed-responsive").each(function()
{
$(this).attr("height", $(this)[0].contentWindow.document.body.scrollHeight.toString() + "px");
});
}
export { ResizeResponsiveEmbeds }

View File

@ -1,4 +1,4 @@
EmptyElementWhenMatches = function(selector, text)
function EmptyElementWhenMatches(selector, text)
{
if ($(selector).text() === text)
{
@ -6,27 +6,12 @@ EmptyElementWhenMatches = function(selector, text)
}
};
String.prototype.contains = function(search)
{
return this.toLowerCase().indexOf(search.toLowerCase()) !== -1;
};
String.prototype.isEmpty = function()
{
return (this.length === 0 || !this.trim());
};
String.prototype.replaceAll = function(search, replacement)
{
return this.replace(new RegExp(search, "g"), replacement);
};
GetUriParam = function(key)
function GetUriParam(key)
{
var currentUri = window.location.search.substring(1);
var vars = currentUri.split('&');
for (i = 0; i < vars.length; i++)
for (var i = 0; i < vars.length; i++)
{
var currentParam = vars[i].split('=');
@ -37,31 +22,22 @@ GetUriParam = function(key)
}
};
UpdateUriParam = function(key, value)
function UpdateUriParam(key, value)
{
var queryParameters = new URLSearchParams(location.search);
queryParameters.set(key, value);
window.history.replaceState({}, "", decodeURIComponent(`${location.pathname}?${queryParameters}`));
};
RemoveUriParam = function(key)
function RemoveUriParam(key)
{
var queryParameters = new URLSearchParams(location.search);
queryParameters.delete(key);
window.history.replaceState({}, "", decodeURIComponent(`${location.pathname}?${queryParameters}`));
};
IsTouchInputDevice = function()
{
if (("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch)
{
return true;
}
return false;
}
BoolVal = function(test)
function BoolVal(test)
{
if (!test)
{
@ -79,12 +55,12 @@ BoolVal = function(test)
}
}
GetFileNameFromPath = function(path)
function GetFileNameFromPath(path)
{
return path.split("/").pop().split("\\").pop();
}
GetFileExtension = function(pathOrFileName)
function GetFileExtension(pathOrFileName)
{
return pathOrFileName.split(".").pop();
}
@ -97,33 +73,6 @@ $.extend($.expr[":"],
}
});
FindObjectInArrayByPropertyValue = function(array, propertyName, propertyValue)
{
for (var i = 0; i < array.length; i++)
{
if (array[i][propertyName] == propertyValue)
{
return array[i];
}
}
return null;
}
FindAllObjectsInArrayByPropertyValue = function(array, propertyName, propertyValue)
{
var returnArray = [];
for (var i = 0; i < array.length; i++)
{
if (array[i][propertyName] == propertyValue)
{
returnArray.push(array[i]);
}
}
return returnArray;
}
$.fn.hasAttr = function(name)
{
@ -190,19 +139,26 @@ function RandomString()
return Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
}
function QrCodeImgHtml(text)
Date.prototype.getWeekNumber = function()
{
var dummyCanvas = document.createElement("canvas");
var img = document.createElement("img");
var date = new Date(Date.UTC(this.getFullYear(), this.getMonth(), this.getDate()));
var dayNumber = date.getUTCDay() || 7;
date.setUTCDate(date.getUTCDate() + 4 - dayNumber);
var yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
return Math.ceil((((date - yearStart) / 86400000) + 1) / 7)
};
bwipjs.toCanvas(dummyCanvas, {
bcid: "qrcode",
text: text,
scale: 4,
includetext: false
});
img.src = dummyCanvas.toDataURL("image/png");
img.classList.add("qr-code");
return img.outerHTML;
}
export
{
RandomString,
animateCSS,
Delay,
IsJsonString,
BoolVal,
GetFileExtension,
GetFileNameFromPath,
RemoveUriParam,
UpdateUriParam,
GetUriParam,
EmptyElementWhenMatches
}

126
js/helpers/frontend.js Normal file
View File

@ -0,0 +1,126 @@
class GrocyFrontendHelpers
{
constructor(Api)
{
this.Api = Api;
}
ValidateForm(formId)
{
var form = document.getElementById(formId);
if (form === null || form === undefined)
{
return;
}
if (form.checkValidity() === true)
{
$(form).find(':submit').removeClass('disabled');
}
else
{
$(form).find(':submit').addClass('disabled');
}
$(form).addClass('was-validated');
}
BeginUiBusy(formId = null)
{
$("body").addClass("cursor-busy");
if (formId !== null)
{
$("#" + formId + " :input").attr("disabled", true);
}
}
EndUiBusy(formId = null)
{
$("body").removeClass("cursor-busy");
if (formId !== null)
{
$("#" + formId + " :input").attr("disabled", false);
}
}
ShowGenericError(message, exception)
{
toastr.error(__t(message) + '<br><br>' + __t('Click to show technical details'), '', {
onclick: function()
{
bootbox.alert({
title: __t('Error details'),
message: '<pre class="my-0"><code>' + JSON.stringify(exception, null, 4) + '</code></pre>',
closeButton: false
});
}
});
console.error(exception);
}
SaveUserSetting(settingsKey, value)
{
Grocy.UserSettings[settingsKey] = value;
var jsonData = {};
jsonData.value = value;
this.Api.Put('user/settings/' + settingsKey, jsonData,
function(result)
{
// Nothing to do...
},
function(xhr)
{
if (!xhr.statusText.isEmpty())
{
this.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
}
);
}
DeleteUserSetting(settingsKey, reloadPageOnSuccess = false)
{
delete Grocy.UserSettings[settingsKey];
this.Delete('user/settings/' + settingsKey, {},
function(result)
{
if (reloadPageOnSuccess)
{
location.reload();
}
},
function(xhr)
{
if (!xhr.statusText.isEmpty())
{
this.ShowGenericError('Error while deleting, please retry', xhr.response)
}
}
);
}
RunWebhook(webhook, data, repetitions = 1)
{
Object.assign(data, webhook.extra_data);
var hasAlreadyFailed = false;
for (i = 0; i < repetitions; i++)
{
$.post(webhook.hook, data).fail(function(req, status, errorThrown)
{
if (!hasAlreadyFailed)
{
hasAlreadyFailed = true;
this.ShowGenericError(__t("Error while executing WebHook", { "status": status, "errorThrown": errorThrown }));
}
});
}
}
}
export { GrocyFrontendHelpers };

23
js/helpers/global.js Normal file
View File

@ -0,0 +1,23 @@
// this file contains everything that needs to be required
// so rollup can be happy.
var jquery = require("jquery");
window.$ = jquery;
window.jQuery = jquery;
window.moment = require("moment");
window.toastr = require("toastr");
window.bootbox = require("bootbox");
// we need to fix these to use the global jquery we included.
var dt = require('datatables.net')(window, jquery);
var dts = require('datatables.net-select')(window, jquery);
var dtsb4 = require('datatables.net-select-bs4')(window, jquery);
var dtb4 = require('datatables.net-bs4')(window, jquery);
var colreorder = require('datatables.net-colreorder')(window, jquery);
var colreorderbs4 = require('datatables.net-colreorder-bs4')(window, jquery);
var rowgroup = require('datatables.net-rowgroup')(window, jquery);
var rowgroupbs4 = require('datatables.net-rowgroup-bs4')(window, jquery);

12
js/helpers/input.js Normal file
View File

@ -0,0 +1,12 @@
function IsTouchInputDevice()
{
if (("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch)
{
return true;
}
return false;
}
export { IsTouchInputDevice }

9
js/helpers/messagebag.js Normal file
View File

@ -0,0 +1,9 @@
function WindowMessageBag(message, payload = null)
{
var obj = {};
obj.Message = message;
obj.Payload = payload;
return obj;
}
export { WindowMessageBag }

View File

@ -0,0 +1,76 @@
function RefreshLocaleNumberDisplay(rootSelector = "#page-content")
{
$(rootSelector + " .locale-number.locale-number-currency").each(function()
{
var text = $(this).text();
if (isNaN(text) || text.isEmpty())
{
return;
}
var value = parseFloat(text);
$(this).text(value.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices }));
});
$(rootSelector + " .locale-number.locale-number-quantity-amount").each(function()
{
var text = $(this).text();
if (isNaN(text) || text.isEmpty())
{
return;
}
var value = parseFloat(text);
$(this).text(value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }));
});
$(rootSelector + " .locale-number.locale-number-generic").each(function()
{
var text = $(this).text();
if (isNaN(text) || text.isEmpty())
{
return;
}
var value = parseFloat(text);
$(this).text(value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 }));
});
}
function RefreshLocaleNumberInput(rootSelector = "#page-content")
{
$(rootSelector + " .locale-number-input.locale-number-currency").each(function()
{
var value = $(this).val();
if (isNaN(value) || value.toString().isEmpty())
{
return;
}
$(this).val(parseFloat(value).toLocaleString("en", { minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices, useGrouping: false }));
});
$(rootSelector + " .locale-number-input.locale-number-quantity-amount").each(function()
{
var value = $(this).val();
if (isNaN(value) || value.toString().isEmpty())
{
return;
}
$(this).val(parseFloat(value).toLocaleString("en", { minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts, useGrouping: false }));
});
$(rootSelector + " .locale-number-input.locale-number-generic").each(function()
{
var value = $(this).val();
if (isNaN(value) || value.toString().isEmpty())
{
return;
}
$(this).val(value.toLocaleString("en", { minimumFractionDigits: 0, maximumFractionDigits: 2, useGrouping: false }));
});
}
export { RefreshLocaleNumberDisplay, RefreshLocaleNumberInput }

21
js/helpers/qrcode.js Normal file
View File

@ -0,0 +1,21 @@
// bwipjs is broken, needs to be explicitly imported.
import bwipJs from '../../node_modules/bwip-js/dist/bwip-js.mjs';
function QrCodeImgHtml(text)
{
var dummyCanvas = document.createElement("canvas");
var img = document.createElement("img");
bwipJs.toCanvas(dummyCanvas, {
bcid: "qrcode",
text: text,
scale: 4,
includetext: false
});
img.src = dummyCanvas.toDataURL("image/png");
img.classList.add("qr-code");
return img.outerHTML;
}
export { QrCodeImgHtml }

15
js/helpers/string.js Normal file
View File

@ -0,0 +1,15 @@
String.prototype.contains = function(search)
{
return this.toLowerCase().indexOf(search.toLowerCase()) !== -1;
};
String.prototype.isEmpty = function()
{
return (this.length === 0 || !this.trim());
};
String.prototype.replaceAll = function(search, replacement)
{
return this.replace(new RegExp(search, "g"), replacement);
};

35
js/lib/UISound.js Normal file
View File

@ -0,0 +1,35 @@
class UISound
{
constructor(Grocy)
{
this.grocy = Grocy;
this.U = path => this.grocy.FormatUrl(path);
}
Play(url)
{
new Audio(url).play();
}
AskForPermission()
{
this.Play(this.U("/uisounds/silence.mp3"));
}
Success()
{
this.Play(this.U("/uisounds/success.mp3"));
}
Error()
{
this.Play(this.U("/uisounds/error.mp3"));
}
BarcodeScannerBeep()
{
this.Play(this.U("/uisounds/barcodescannerbeep.mp3"));
}
}
export { UISound }

87
js/lib/WakeLock.js Normal file
View File

@ -0,0 +1,87 @@
import { BoolVal } from '../helpers/extensions';
// this class has side-effects and only works as a singleton. GrocyClass is responsible for handling that.
class WakeLock
{
constructor(Grocy)
{
this.NoSleepJsIntance = null;
this.InitDone = false;
this.grocy = Grocy;
var self = this; // jquery probably overrides this
$("#keep_screen_on").on("change", function()
{
var value = $(this).is(":checked");
if (value)
{
self.Enable();
}
else
{
self.Disable();
}
});
this.observer = // Handle "Keep screen on while displaying a fullscreen-card" when the body class "fullscreen-card" has changed
new MutationObserver(function(mutations)
{
if (BoolVal(Grocy.UserSettings.keep_screen_on_when_fullscreen_card) && !BoolVal(Grocy.UserSettings.keep_screen_on))
{
mutations.forEach(function(mutation)
{
if (mutation.attributeName === "class")
{
var attributeValue = $(mutation.target).prop(mutation.attributeName);
if (attributeValue.contains("fullscreen-card"))
{
self.Enable();
}
else
{
self.Disable();
}
}
});
}
});
this.observer.observe(document.body, {
attributes: true
});
// Enabling NoSleep.Js only works in a user input event handler,
// so if the user wants to keep the screen on always,
// do this in on the first click on anything
$(document).click(function()
{
if (Grocy.WakeLock.InitDone === false && BoolVal(Grocy.UserSettings.keep_screen_on))
{
self.Enable();
}
self.InitDone = true;
});
}
Enable()
{
if (this.NoSleepJsIntance === null)
{
this.NoSleepJsIntance = new NoSleep();
}
this.NoSleepJsIntance.enable();
this.InitDone = true;
}
Disable()
{
if (this.NoSleepJsIntance !== null)
{
this.NoSleepJsIntance.disable();
}
}
}
export { WakeLock }

235
js/lib/api.js Normal file
View File

@ -0,0 +1,235 @@
class GrocyApi
{
constructor(Grocy)
{
this.Grocy = Grocy;
}
Get(apiFunction, success, error)
{
var xhr = new XMLHttpRequest();
var url = this.Grocy.FormatUrl('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200 || xhr.status === 204)
{
if (success)
{
if (xhr.status === 200)
{
success(JSON.parse(xhr.responseText));
}
else
{
success({});
}
}
}
else
{
if (error)
{
error(xhr);
}
}
}
}
xhr.open('GET', url, true);
xhr.send();
}
Post(apiFunction, jsonData, success, error)
{
var xhr = new XMLHttpRequest();
var url = this.Grocy.FormatUrl('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200 || xhr.status === 204)
{
if (success)
{
if (xhr.status === 200)
{
success(JSON.parse(xhr.responseText));
}
else
{
success({});
}
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(jsonData));
}
Put(apiFunction, jsonData, success, error)
{
var xhr = new XMLHttpRequest();
var url = this.Grocy.FormatUrl('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200 || xhr.status === 204)
{
if (success)
{
if (xhr.status === 200)
{
success(JSON.parse(xhr.responseText));
}
else
{
success({});
}
}
}
else
{
if (error)
{
error(xhr);
}
}
}
}
xhr.open('PUT', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(jsonData));
}
Delete(apiFunction, jsonData, success, error)
{
var xhr = new XMLHttpRequest();
var url = this.Grocy.FormatUrl('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200 || xhr.status === 204)
{
if (success)
{
if (xhr.status === 200)
{
success(JSON.parse(xhr.responseText));
}
else
{
success({});
}
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('DELETE', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(jsonData));
}
UploadFile(file, group, fileName, success, error)
{
var xhr = new XMLHttpRequest();
var url = this.Grocy.FormatUrl('/api/files/' + group + '/' + btoa(fileName));
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200 || xhr.status === 204)
{
if (success)
{
if (xhr.status === 200)
{
success(JSON.parse(xhr.responseText));
}
else
{
success({});
}
}
}
else
{
if (error)
{
error(xhr);
}
}
}
}
xhr.open('PUT', url, true);
xhr.setRequestHeader('Content-type', 'application/octet-stream');
xhr.send(file);
}
DeleteFile(fileName, group, success, error)
{
var xhr = new XMLHttpRequest();
var url = this.Grocy.FormatUrl('/api/files/' + group + '/' + btoa(fileName));
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200 || xhr.status === 204)
{
if (success)
{
if (xhr.status === 200)
{
success(JSON.parse(xhr.responseText));
}
else
{
success({});
}
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('DELETE', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send();
}
}
export { GrocyApi };

141
js/lib/nightmode.js Normal file
View File

@ -0,0 +1,141 @@
import { BoolVal } from '../helpers/extensions'
class Nightmode
{
constructor(Grocy)
{
var self = this;
this.Grocy = Grocy;
$("#night-mode-enabled").on("change", function()
{
var value = $(this).is(":checked");
if (value)
{
// Force disable auto night mode when night mode is enabled
$("#auto-night-mode-enabled").prop("checked", false);
$("#auto-night-mode-enabled").trigger("change");
$("body").addClass("night-mode");
}
else
{
$("body").removeClass("night-mode");
}
});
$("#auto-night-mode-enabled").on("change", function()
{
var value = $(this).is(":checked");
$("#auto-night-mode-time-range-from").prop("readonly", !value);
$("#auto-night-mode-time-range-to").prop("readonly", !value);
if (!value && !BoolVal(self.Grocy.UserSettings.night_mode_enabled))
{
$("body").removeClass("night-mode");
}
// Force disable night mode when auto night mode is enabled
if (value)
{
$("#night-mode-enabled").prop("checked", false);
$("#night-mode-enabled").trigger("change");
}
});
$(document).on("keyup", "#auto-night-mode-time-range-from, #auto-night-mode-time-range-to", function()
{
var value = $(this).val();
var valueIsValid = moment(value, "HH:mm", true).isValid();
if (valueIsValid)
{
$(this).removeClass("bg-danger");
}
else
{
$(this).addClass("bg-danger");
}
self.CheckNightMode();
});
$("#auto-night-mode-time-range-goes-over-midgnight").on("change", function()
{
self.CheckNightMode();
});
if (this.Grocy.UserId !== -1)
{
$("#night-mode-enabled").prop("checked", BoolVal(this.Grocy.UserSettings.night_mode_enabled));
$("#auto-night-mode-enabled").prop("checked", BoolVal(this.Grocy.UserSettings.auto_night_mode_enabled));
$("#auto-night-mode-time-range-goes-over-midgnight").prop("checked", BoolVal(this.Grocy.UserSettings.auto_night_mode_time_range_goes_over_midnight));
$("#auto-night-mode-enabled").trigger("change");
$("#auto-night-mode-time-range-from").val(this.Grocy.UserSettings.auto_night_mode_time_range_from);
$("#auto-night-mode-time-range-from").trigger("keyup");
$("#auto-night-mode-time-range-to").val(this.Grocy.UserSettings.auto_night_mode_time_range_to);
$("#auto-night-mode-time-range-to").trigger("keyup");
}
}
StartWatchdog()
{
if (this.Grocy.UserId !== -1)
{
this.CheckNightMode();
}
if (this.Grocy.Mode === "production")
{
setInterval(this.CheckNightMode, 60000);
}
else
{
setInterval(this.CheckNightMode, 4000);
}
}
CheckNightMode()
{
if (this.Grocy.UserId === -1 || !BoolVal(this.Grocy.UserSettings.auto_night_mode_enabled))
{
return;
}
var start = moment(this.Grocy.UserSettings.auto_night_mode_time_range_from, "HH:mm", true);
var end = moment(this.Grocy.UserSettings.auto_night_mode_time_range_to, "HH:mm", true);
var now = moment();
if (!start.isValid() || !end.isValid)
{
return;
}
if (BoolVal(this.Grocy.UserSettings.auto_night_mode_time_range_goes_over_midnight))
{
end.add(1, "day");
}
if (start.isSameOrBefore(now) && end.isSameOrAfter(now)) // We're INSIDE of night mode time range
{
if (!$("body").hasClass("night-mode"))
{
$("body").addClass("night-mode");
$("#currently-inside-night-mode-range").prop("checked", true);
$("#currently-inside-night-mode-range").trigger("change");
}
}
else // We're OUTSIDE of night mode time range
{
if ($("body").hasClass("night-mode"))
{
$("body").removeClass("night-mode");
$("#currently-inside-night-mode-range").prop("checked", false);
$("#currently-inside-night-mode-range").trigger("change");
}
}
}
}
export { Nightmode }

20
js/vendor.js Normal file
View File

@ -0,0 +1,20 @@
// Library bundle.
// this will be split-up so that tree shaking works and bundle size
// will be reduced, but at the moment, I need this as-is.
import "./helpers/global";
import "bootstrap";
import "popper.js";
import "startbootstrap-sb-admin/js/sb-admin";
import "jquery-serializejson";
import "@danielfarrell/bootstrap-combobox/js/bootstrap-combobox";
import "datatables.net-plugins/filtering/type-based/accent-neutralise";
import "datatables.net-plugins/sorting/chinese-string";
import "timeago/jquery.timeago";
import "tempusdominus-bootstrap-4/build/js/tempusdominus-bootstrap-4";
import "sprintf-js/src/sprintf";
import "summernote/dist/summernote-bs4";
import "bootstrap-select";
import "jquery-lazy";
import "nosleep.js";

View File

@ -1,4 +1,6 @@
$('#save-battery-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-battery-button').on('click', function(e)
{
e.preventDefault();

View File

@ -17,7 +17,7 @@
function(result)
{
Grocy.FrontendHelpers.EndUiBusy("batterytracking-form");
toastr.success(__t('Tracked charge cycle of battery %1$s on %2$s', batteryDetails.battery.name, $('#tracked_time').find('input').val()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChargeCycle(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
toastr.success(__t('Tracked charge cycle of battery %1$s on %2$s', batteryDetails.battery.name, $('#tracked_time').find('input').val()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="Grocy.UndoChargeCycle(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
Grocy.Components.BatteryCard.Refresh($('#battery_id').val());
$('#battery_id').val('');
@ -97,16 +97,4 @@ $('#tracked_time').find('input').on('keypress', function(e)
Grocy.FrontendHelpers.ValidateForm('batterytracking-form');
});
function UndoChargeCycle(chargeCycleId)
{
Grocy.Api.Post('batteries/charge-cycles/' + chargeCycleId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Charge cycle successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

90
js/viewjs/calendar.js Normal file
View File

@ -0,0 +1,90 @@
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import bootstrapPlugin from '@fullcalendar/bootstrap';
import listPlugin from '@fullcalendar/list';
import timeGridPlugin from '@fullcalendar/timegrid';
import { QrCodeImgHtml } from "../helpers/qrcode";
import '@fullcalendar/core/main.css';
import '@fullcalendar/daygrid/main.css';
import '@fullcalendar/timegrid/main.css';
import '@fullcalendar/list/main.css';
import '@fullcalendar/bootstrap/main.css';
var calendarOptions = {
plugins: [bootstrapPlugin, dayGridPlugin, listPlugin, timeGridPlugin],
themeSystem: "bootstrap",
header: {
left: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
center: "title",
right: "prev,next"
},
weekNumbers: Grocy.CalendarShowWeekNumbers,
defaultView: ($(window).width() < 768) ? "timeGridDay" : "dayGridMonth",
firstDay: firstDay,
eventLimit: false,
height: "auto",
events: fullcalendarEventSources,
// fullcalendar 4 doesn't translate the default view names (?)
// so we have to supply our own.
views: {
dayGridMonth: { buttonText: __t("Month") },
timeGridWeek: { buttonText: __t("Week") },
timeGridDay: { buttonText: __t("Day") },
listWeek: { buttonText: __t("List") }
},
eventClick: function(info)
{
location.href = info.link;
}
};
if (__t('fullcalendar_locale').replace(" ", "") !== "" && __t('fullcalendar_locale') != 'x')
{
$.getScript(U('/js/locales/fullcalendar-core/' + __t('fullcalendar_locale') + '.js'));
calendarOptions.locale = __t('fullcalendar_locale');
}
var firstDay = null;
if (!Grocy.CalendarFirstDayOfWeek.isEmpty())
{
firstDay = parseInt(Grocy.CalendarFirstDayOfWeek);
}
var calendar = new Calendar(document.getElementById("calendar"), calendarOptions);
calendar.render();
$("#ical-button").on("click", function(e)
{
e.preventDefault();
Grocy.Api.Get('calendar/ical/sharing-link',
function(result)
{
bootbox.alert({
title: __t('Share/Integrate calendar (iCal)'),
message: __t('Use the following (public) URL to share or integrate the calendar in iCal format') + '<input type="text" class="form-control form-control-sm mt-2 easy-link-copy-textbox" value="' + result.url + '"><p class="text-center mt-4">'
+ QrCodeImgHtml(result.url) + "</p>",
closeButton: false
});
},
function(xhr)
{
console.error(xhr);
}
);
});
$(window).one("resize", function()
{
// Automatically switch the calendar to "basicDay" view on small screens
// and to "month" otherwise
if ($(window).width() < 768)
{
calendar.changeView("timeGridDay");
}
else
{
calendar.changeView("dayGridMonth");
}
});

View File

@ -17,7 +17,7 @@
function(result)
{
Grocy.FrontendHelpers.EndUiBusy("choretracking-form");
toastr.success(__t('Tracked execution of chore %1$s on %2$s', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoChoreExecution(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
toastr.success(__t('Tracked execution of chore %1$s on %2$s', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="Grocy.UndoChoreExecution(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
Grocy.Components.ChoreCard.Refresh($('#chore_id').val());
$('#chore_id').val('');
@ -113,17 +113,3 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
{
Grocy.FrontendHelpers.ValidateForm('choretracking-form');
});
function UndoChoreExecution(executionId)
{
Grocy.Api.Post('chores/executions/' + executionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Chore execution successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@ -1,3 +1,5 @@
import Quagga from '@ericblade/quagga2/dist/quagga';
Grocy.Components.BarcodeScanner = {};
//import Quagga2DatamatrixReader from '../../components_unmanaged/quagga2-reader-datamatrix/index.js'
@ -121,6 +123,8 @@ Grocy.Components.BarcodeScanner.StartScanning = function()
locate: true
}, function(error)
{
// error *needs* to be logged here, otherwise the stack trace is lying.
console.error(error);
if (error)
{
Grocy.FrontendHelpers.ShowGenericError("Error while initializing the barcode scanning library", error.message);
@ -226,7 +230,7 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
Grocy.Components.BarcodeScanner.CurrentTarget = inputElement.attr("data-target");
var dialog = bootbox.dialog({
message: '<div id="barcodescanner-container" class="col"><div id="barcodescanner-livestream"></div></div>',
message: '<div id="barcodescanner-container" class="col"><div id="barcodescanner-livestream"></div><div id="debug"></div></div>',
title: __t('Scan a barcode'),
onEscape: function()
{
@ -260,24 +264,27 @@ $(document).on("click", "#barcodescanner-start-button", async function(e)
dialog.find('.bootbox-body').append('<div class="form-group py-0 my-1 cameraSelect-wrapper"><select class="custom-control custom-select cameraSelect"><select class="custom-control custom-select cameraSelect" style="display: none"></select></div>');
var cameraSelect = document.querySelector('.cameraSelect');
var cameras = await Quagga.CameraAccess.enumerateVideoDevices();
cameras.forEach(camera =>
if (cameraSelect != null)
{
var option = document.createElement("option");
option.text = camera.label ? camera.label : camera.deviceId; // Use camera label if it exists, else show device id
option.value = camera.deviceId;
cameraSelect.appendChild(option);
});
var cameras = await Quagga.CameraAccess.enumerateVideoDevices();
cameras.forEach(camera =>
{
var option = document.createElement("option");
option.text = camera.label ? camera.label : camera.deviceId; // Use camera label if it exists, else show device id
option.value = camera.deviceId;
cameraSelect.appendChild(option);
});
// Set initial value to preferred camera if one exists - and if not, start out empty
cameraSelect.value = window.localStorage.getItem('cameraId');
// Set initial value to preferred camera if one exists - and if not, start out empty
cameraSelect.value = window.localStorage.getItem('cameraId');
cameraSelect.onchange = function()
{
window.localStorage.setItem('cameraId', cameraSelect.value);
Quagga.stop();
Grocy.Components.BarcodeScanner.StartScanning();
};
cameraSelect.onchange = function()
{
window.localStorage.setItem('cameraId', cameraSelect.value);
Quagga.stop();
Grocy.Components.BarcodeScanner.StartScanning();
};
}
Grocy.Components.BarcodeScanner.StartScanning();
});

View File

@ -34,7 +34,7 @@ Grocy.Components.DateTimePicker.Clear = function()
// "Click" the shortcut checkbox when the desired value is
// not the shortcut value and it is currently set
value = "";
var value = "";
var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value");
if (value != shortcutValue && $("#datetimepicker-shortcut").is(":checked"))
{

View File

@ -43,7 +43,7 @@ $('.location-combobox').combobox({
var prefillByName = Grocy.Components.LocationPicker.GetPicker().parent().data('prefill-by-name').toString();
if (typeof prefillByName !== "undefined")
{
possibleOptionElement = $("#location_id option:contains(\"" + prefillByName + "\")").first();
var possibleOptionElement = $("#location_id option:contains(\"" + prefillByName + "\")").first();
if (possibleOptionElement.length > 0)
{

View File

@ -3,13 +3,14 @@ Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled = false;
Grocy.Components.ProductAmountPicker.Reload = function(productId, destinationQuId, forceInitialDisplayQu = false)
{
var conversionsForProduct = FindAllObjectsInArrayByPropertyValue(Grocy.QuantityUnitConversionsResolved, 'product_id', productId);
var conversionsForProduct = Grocy.QuantityUnitConversionsResolved.filter(elem => elem.product_id == productId);
if (!Grocy.Components.ProductAmountPicker.AllowAnyQuEnabled)
{
var qu = Grocy.QuantityUnits.find(elem => elem.id == destinationQuId);
$("#qu_id").find("option").remove().end();
$("#qu_id").attr("data-destination-qu-name", FindObjectInArrayByPropertyValue(Grocy.QuantityUnits, 'id', destinationQuId).name);
$("#qu_id").attr("data-destination-qu-name-plural", FindObjectInArrayByPropertyValue(Grocy.QuantityUnits, 'id', destinationQuId).name_plural);
$("#qu_id").attr("data-destination-qu-name", qu.name);
$("#qu_id").attr("data-destination-qu-name-plural", qu.name_plural);
conversionsForProduct.forEach(conversion =>
{

View File

@ -1,3 +1,5 @@
import Chart from 'chart.js';
Grocy.Components.ProductCard = {};
Grocy.Components.ProductCard.Refresh = function(productId)

View File

@ -43,7 +43,7 @@ $('.recipe-combobox').combobox({
var prefillByName = Grocy.Components.RecipePicker.GetPicker().parent().data('prefill-by-name').toString();
if (typeof prefillByName !== "undefined")
{
possibleOptionElement = $("#recipe_id option:contains(\"" + prefillByName + "\")").first();
var possibleOptionElement = $("#recipe_id option:contains(\"" + prefillByName + "\")").first();
if (possibleOptionElement.length > 0)
{

View File

@ -43,7 +43,7 @@ $('.shopping-location-combobox').combobox({
var prefillByName = Grocy.Components.ShoppingLocationPicker.GetPicker().parent().data('prefill-by-name').toString();
if (typeof prefillByName !== "undefined")
{
possibleOptionElement = $("#shopping_location_id option:contains(\"" + prefillByName + "\")").first();
var possibleOptionElement = $("#shopping_location_id option:contains(\"" + prefillByName + "\")").first();
if (possibleOptionElement.length > 0)
{

View File

@ -1,4 +1,7 @@
$('#save-consume-button').on('click', function(e)
import { BoolVal } from '../helpers/extensions';
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-consume-button').on('click', function(e)
{
e.preventDefault();
@ -77,11 +80,11 @@
if (productDetails.product.enable_tare_weight_handling == 1 && !jsonData.exact_amount)
{
var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - (parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_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="UndoStockTransaction(\'' + bookingResponse[0].transaction_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) + parseFloat(productDetails.stock_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="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
}
else
{
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="UndoStockTransaction(\'' + bookingResponse[0].transaction_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="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
}
if (GetUriParam("embedded") !== undefined)
@ -154,7 +157,7 @@ $('#save-mark-as-open-button').on('click', function(e)
var apiUrl = 'stock/products/' + jsonForm.product_id + '/open';
jsonData = {};
var jsonData = {};
jsonData.amount = jsonForm.amount;
jsonData.allow_subproduct_substitution = true;
@ -176,7 +179,7 @@ $('#save-mark-as-open-button').on('click', function(e)
}
Grocy.FrontendHelpers.EndUiBusy("consume-form");
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(jsonForm.amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(jsonForm.amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="Grocy.UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_amount))
{
@ -548,34 +551,6 @@ $("#qu_id").on("change", function()
RefreshForm();
});
function UndoStockBooking(bookingId)
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
function UndoStockTransaction(transactionId)
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Transaction successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
if (GetUriParam("embedded") !== undefined)
{
var locationId = GetUriParam('locationId');

View File

@ -1,4 +1,6 @@
var equipmentTable = $('#equipment-table').DataTable({
import { ResizeResponsiveEmbeds } from "../helpers/embeds";
var equipmentTable = $('#equipment-table').DataTable({
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },

View File

@ -1,4 +1,6 @@
$('#save-inventory-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-inventory-button').on('click', function(e)
{
e.preventDefault();
@ -70,7 +72,7 @@
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="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
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="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
if (GetUriParam("embedded") !== undefined)
{
@ -359,32 +361,4 @@ $('#display_amount').on('keyup', function(e)
}
});
function UndoStockBooking(bookingId)
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
function UndoStockTransaction(transactionId)
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Transaction successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
$("#display_amount").attr("min", "0");

View File

@ -1,4 +1,6 @@
$('#save-location-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-location-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,21 @@
var firstRender = true;
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import bootstrapPlugin from '@fullcalendar/bootstrap';
import momentPlugin from '@fullcalendar/moment/main';
import { toMoment } from '@fullcalendar/moment/main';
import '@fullcalendar/core/main.css';
import '@fullcalendar/daygrid/main.css';
import '@fullcalendar/bootstrap/main.css';
var setLocale = false;
if (__t('fullcalendar_locale').replace(" ", "") !== "" && __t('fullcalendar_locale') != 'x')
{
setLocale = true;
$.getScript(U('/js/locales/fullcalendar-core/' + __t('fullcalendar_locale') + '.js'));
}
var firstRender = true;
Grocy.IsMealPlanEntryEditAction = false;
Grocy.MealPlanEntryEditObjectId = -1;
@ -12,28 +29,31 @@ if (!Grocy.MealPlanFirstDayOfWeek.isEmpty())
firstDay = parseInt(Grocy.MealPlanFirstDayOfWeek);
}
var calendar = $("#calendar").fullCalendar({
"themeSystem": "bootstrap4",
"header": {
"left": "title",
"center": "",
"right": "prev,today,next"
var calendar = new Calendar(document.getElementById("calendar"), {
plugins: [dayGridPlugin, bootstrapPlugin, momentPlugin],
themeSystem: "bootstrap",
header: {
left: "title",
center: "",
right: "prev,today,next"
},
"weekNumbers": false,
"eventLimit": false,
"eventSources": fullcalendarEventSources,
"defaultView": ($(window).width() < 768) ? "basicDay" : "basicWeek",
"firstDay": firstDay,
"height": "auto",
"viewRender": function(view)
weekNumbers: false,
eventLimit: false,
events: fullcalendarEventSources,
defaultView: ($(window).width() < 768) ? "dayGridDay" : "dayGridWeek",
firstDay: firstDay,
height: "auto",
datesRender: function(info)
{
var view = info.view;
var start = toMoment(view.activeStart, calendar);
if (firstRender)
{
firstRender = false
}
else
{
UpdateUriParam("week", view.start.format("YYYY-MM-DD"));
UpdateUriParam("week", start.format("YYYY-MM-DD"));
}
$(".fc-day-header").prepend('\
@ -46,29 +66,30 @@ var calendar = $("#calendar").fullCalendar({
</div> \
</div>');
var weekRecipeName = view.start.year().toString() + "-" + ((view.start.week() - 1).toString().padStart(2, "0")).toString();
var weekRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", weekRecipeName);
var weekRecipeName = start.year() + "-" + ((start.week() - 1).toString().padStart(2, "0")).toString();
var weekRecipe = internalRecipes.find(elem => elem.name == weekRecipeName);
var weekCosts = 0;
var weekRecipeOrderMissingButtonHtml = "";
var weekRecipeConsumeButtonHtml = "";
var weekCostsHtml = "";
if (weekRecipe !== null)
if (weekRecipe !== null && weekRecipe !== undefined) // Array.prototype.find returns undefined if not found.
{
var recipes = recipesResolved.find(elem => elem.recipe_id == weekRecipe.id);
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs;
weekCosts = recipes.costs;
weekCostsHtml = __t("Week costs") + ': <span class="locale-number locale-number-currency">' + weekCosts.toString() + "</span> ";
}
var weekRecipeOrderMissingButtonDisabledClasses = "";
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled_with_shopping_list == 1)
if (recipes.need_fulfilled_with_shopping_list == 1)
{
weekRecipeOrderMissingButtonDisabledClasses = "disabled";
}
var weekRecipeConsumeButtonDisabledClasses = "";
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0 || weekCosts == 0)
if (recipes.need_fulfilled == 0 || weekCosts == 0)
{
weekRecipeConsumeButtonDisabledClasses = "disabled";
}
@ -77,8 +98,10 @@ var calendar = $("#calendar").fullCalendar({
}
$(".fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
},
"eventRender": function(event, element)
eventRender: function(info)
{
var event = info.event.extendedProps;
var element = $(info.el);
element.removeClass("fc-event");
element.addClass("text-center");
element.attr("data-meal-plan-entry", event.mealPlanEntry);
@ -93,7 +116,7 @@ var calendar = $("#calendar").fullCalendar({
return false;
}
var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", recipe.id);
var resolvedRecipe = recipesResolved.find(elem => elem.recipe_id == recipe.id);
element.attr("data-recipe", event.recipe);
@ -152,13 +175,13 @@ var calendar = $("#calendar").fullCalendar({
element.html(element.html() + '<div class="mx-auto"><img data-src="' + U("/api/files/recipepictures/") + btoa(recipe.picture_file_name) + '?force_serve_as=picture&best_fit_width=400" class="img-fluid lazy"></div>')
}
var dayRecipeName = event.start.format("YYYY-MM-DD");
var dayRecipeName = toMoment(info.event.start, calendar).format("YYYY-MM-DD");
if (!$("#day-summary-" + dayRecipeName).length) // This runs for every event/recipe, so maybe multiple times per day, so only add the day summary once
{
var dayRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", dayRecipeName);
var dayRecipe = internalRecipes.find(elem => elem.name == dayRecipeName);
if (dayRecipe != null)
{
var dayRecipeResolved = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", dayRecipe.id);
var dayRecipeResolved = recipesResolved.find(elem => elem.recipe_id == dayRecipe.id);
var costsAndCaloriesPerDay = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@ -237,13 +260,13 @@ var calendar = $("#calendar").fullCalendar({
element.html(element.html() + '<div class="mx-auto"><img data-src="' + U("/api/files/productpictures/") + btoa(productDetails.product.picture_file_name) + '?force_serve_as=picture&best_fit_width=400" class="img-fluid lazy"></div>')
}
var dayRecipeName = event.start.format("YYYY-MM-DD");
var dayRecipeName = toMoment(info.event.start, calendar).format("YYYY-MM-DD");
if (!$("#day-summary-" + dayRecipeName).length) // This runs for every event/recipe, so maybe multiple times per day, so only add the day summary once
{
var dayRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", dayRecipeName);
var dayRecipe = internalRecipes.find(elem => elem.name == dayRecipeName);
if (dayRecipe != null)
{
var dayRecipeResolved = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", dayRecipe.id);
var dayRecipeResolved = recipesResolved.find(elem => elem.recipe_id == dayRecipe.id);
var costsAndCaloriesPerDay = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
@ -270,24 +293,36 @@ var calendar = $("#calendar").fullCalendar({
</div>');
}
},
"eventAfterAllRender": function(view)
eventPositioned: function(info)
{
// this callback is called once a event is rendered.
// try to limit DOM operations as much as possible
// to the rendered element.
var elem = $(info.el);
RefreshLocaleNumberDisplay();
LoadImagesLazy();
$('[data-toggle="tooltip"]').tooltip();
if (GetUriParam("week") !== undefined)
{
$("#calendar").fullCalendar("gotoDate", GetUriParam("week"));
}
elem.find('[data-toggle="tooltip"]').tooltip();
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK)
{
$(".recipe-order-missing-button").addClass("d-none");
$(".recipe-consume-button").addClass("d-none");
elem.find(".recipe-order-missing-button").addClass("d-none");
elem.find(".recipe-consume-button").addClass("d-none");
}
},
});
// render the calendar.
calendar.render();
if (setLocale)
{
calendar.setLocale(__t('fullcalendar_locale'));
}
// this triggers a re-render, so we can't do that in the callback;
// but it works here no problem.
if (GetUriParam("week") !== undefined)
{
calendar.gotoDate(GetUriParam("week"));
}
$(document).on("click", ".add-recipe-button", function(e)
{
@ -662,7 +697,7 @@ $(document).on('click', '.product-consume-button', function(e)
Grocy.Api.Get('stock/products/' + productId,
function(result)
{
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="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
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="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
Grocy.FrontendHelpers.EndUiBusy();
toastr.success(toastMessage);
@ -784,11 +819,11 @@ $(window).one("resize", function()
// and to "basicWeek" otherwise
if ($(window).width() < 768)
{
calendar.fullCalendar("changeView", "basicDay");
calendar.changeView("dayGridDay");
}
else
{
calendar.fullCalendar("changeView", "basicWeek");
calendar.changeView("dayGridWeek");
}
});

View File

@ -1,4 +1,6 @@
$('#save-barcode-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-barcode-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
function saveProductPicture(result, location, jsonData)
import { BoolVal } from '../helpers/extensions';
function saveProductPicture(result, location, jsonData)
{
var productId = Grocy.EditObjectId || result.created_object_id;

View File

@ -1,4 +1,6 @@
$('#save-product-group-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-product-group-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,7 @@
var CurrentProductDetails;
import { BoolVal } from '../helpers/extensions';
import { WindowMessageBag } from '../helpers/messagebag';
var CurrentProductDetails;
$('#save-purchase-button').on('click', function(e)
{
@ -115,7 +118,7 @@ $('#save-purchase-button').on('click', function(e)
{
amountMessage = parseFloat(jsonForm.amount) - parseFloat(productDetails.stock_amount) - parseFloat(productDetails.product.tare_weight);
}
var successMessage = __t('Added %1$s of %2$s to stock', amountMessage + " " + __n(amountMessage, 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="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var successMessage = __t('Added %1$s of %2$s to stock', amountMessage + " " + __n(amountMessage, 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="Grocy.UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABELPRINTER)
{
@ -330,8 +333,8 @@ if (Grocy.Components.ProductPicker !== undefined)
}
RefreshLocaleNumberInput();
if (document.getElementById("product_id").getAttribute("barcode") != "null")
var elem = document.getElementById("product_id");
if (elem.getAttribute("barcode") != "null" && !elem.getAttribute("barcode").startsWith("grcy"))
{
Grocy.Api.Get('objects/product_barcodes?query[]=barcode=' + document.getElementById("product_id").getAttribute("barcode"),
function(barcodeResult)
@ -536,55 +539,6 @@ function refreshPriceHint()
}
};
function UndoStockBooking(bookingId)
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
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)
{
console.error(xhr);
}
);
};
function UndoStockTransaction(transactionId)
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Transaction successfully undone"));
Grocy.Api.Get('stock/transactions/' + transactionId.toString(),
function(result)
{
window.postMessage(WindowMessageBag("ProductChanged", result[0].product_id), Grocy.BaseUrl);
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
};
$("#scan-mode").on("change", function(e)
{

View File

@ -1,4 +1,6 @@
$('#save-quconversion-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-quconversion-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
$('.save-quantityunit-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('.save-quantityunit-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
function saveRecipePicture(result, location, jsonData)
import { WindowMessageBag } from '../helpers/messagebag';
function saveRecipePicture(result, location, jsonData)
{
var recipeId = Grocy.EditObjectId || result.created_object_id;
Grocy.Components.UserfieldsForm.Save(() =>

View File

@ -1,4 +1,5 @@
Grocy.RecipePosFormInitialLoadDone = false;
import { WindowMessageBag } from '../helpers/messagebag';
Grocy.RecipePosFormInitialLoadDone = false;
$('#save-recipe-pos-button').on('click', function(e)
{

View File

@ -0,0 +1,8 @@
import { BoolVal } from '../helpers/extensions';
if (BoolVal(Grocy.UserSettings.recipe_ingredients_group_by_product_group))
{
$("#recipe_ingredients_group_by_product_group").prop("checked", true);
}
RefreshLocaleNumberInput();

View File

@ -1,4 +1,9 @@
var shoppingListTable = $('#shoppinglist-table').DataTable({
// this needs to be explicitly imported for some reason,
// otherwise rollup complains.
import bwipjs from '../../node_modules/bwip-js/dist/bwip-js.mjs';
import { WindowMessageBag } from '../helpers/messagebag';
var shoppingListTable = $('#shoppinglist-table').DataTable({
'order': [[1, 'asc']],
"orderFixed": [[3, 'asc']],
'columnDefs': [

View File

@ -1,4 +1,6 @@
$('#save-shopping-list-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-shopping-list-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
Grocy.ShoppingListItemFormInitialLoadDone = false;
import { WindowMessageBag } from '../helpers/messagebag';
Grocy.ShoppingListItemFormInitialLoadDone = false;
$('#save-shoppinglist-button').on('click', function(e)
{

View File

@ -1,4 +1,6 @@
if (BoolVal(Grocy.UserSettings.shopping_list_to_stock_workflow_auto_submit_when_prefilled))
import { BoolVal } from '../helpers/extensions';
if (BoolVal(Grocy.UserSettings.shopping_list_to_stock_workflow_auto_submit_when_prefilled))
{
$("#shopping-list-to-stock-workflow-auto-submit-when-prefilled").prop("checked", true);
}

View File

@ -1,4 +1,6 @@
$('#save-shopping-location-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-shopping-location-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
var stockEntriesTable = $('#stockentries-table').DataTable({
import { WindowMessageBag } from '../helpers/messagebag';
var stockEntriesTable = $('#stockentries-table').DataTable({
'order': [[2, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
@ -60,7 +62,7 @@ $(document).on('click', '.stock-consume-button', function(e)
Grocy.Api.Get('stock/products/' + productId,
function(result)
{
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(consumeAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="UndoStockBookingEntry(' + bookingResponse.id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(consumeAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="Grocy.UndoStockBookingEntry(' + bookingResponse.id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
if (wasSpoiled)
{
toastMessage += " (" + __t("Spoiled") + ")";
@ -107,7 +109,7 @@ $(document).on('click', '.product-open-button', function(e)
{
button.addClass("disabled");
Grocy.FrontendHelpers.EndUiBusy();
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="UndoStockBookingEntry(' + bookingResponse.id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
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="Grocy.UndoStockBookingEntry(' + bookingResponse.id + ',' + stockRowId + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
RefreshStockEntryRow(stockRowId);
},
function(xhr)
@ -266,21 +268,6 @@ $(window).on("message", function(e)
Grocy.Components.ProductPicker.GetPicker().trigger('change');
function UndoStockBookingEntry(bookingId, stockRowId)
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result)
{
window.postMessage(WindowMessageBag("StockEntryChanged", stockRowId), Grocy.BaseUrl);
toastr.success(__t("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
$(document).on("click", ".product-name-cell", function(e)
{
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id"));

View File

@ -1,4 +1,6 @@
$('#save-stockentry-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-stockentry-button').on('click', function(e)
{
e.preventDefault();
@ -38,7 +40,7 @@
Grocy.Api.Put("stock/entry/" + Grocy.EditObjectId, jsonData,
function(result)
{
var successMessage = __t('Stock entry successfully updated') + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBookingEntry(\'' + result.id + '\',\'' + Grocy.EditObjectId + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var successMessage = __t('Stock entry successfully updated') + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="Grocy.UndoStockBookingEntry(\'' + result.id + '\',\'' + Grocy.EditObjectId + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
window.parent.postMessage(WindowMessageBag("StockEntryChanged", Grocy.EditObjectId), Grocy.BaseUrl);
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);

View File

@ -139,11 +139,11 @@ $(document).on('click', '.product-consume-button', function(e)
{
if (result.product.enable_tare_weight_handling == 1)
{
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(originalTotalStockAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(originalTotalStockAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
}
else
{
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(consumeAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var toastMessage = __t('Removed %1$s of %2$s from stock', parseFloat(consumeAmount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + __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="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
}
if (wasSpoiled)
@ -199,7 +199,7 @@ $(document).on('click', '.product-open-button', function(e)
}
Grocy.FrontendHelpers.EndUiBusy();
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + productQuName, productName) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
toastr.success(__t('Marked %1$s of %2$s as opened', parseFloat(amount).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_amounts }) + " " + productQuName, productName) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
RefreshStatistics();
RefreshProductRow(productId);
},

View File

@ -1,4 +1,6 @@
$("#product_presets_location_id").val(Grocy.UserSettings.product_presets_location_id);
import { BoolVal } from '../helpers/extensions';
$("#product_presets_location_id").val(Grocy.UserSettings.product_presets_location_id);
$("#product_presets_product_group_id").val(Grocy.UserSettings.product_presets_product_group_id);
$("#product_presets_qu_id").val(Grocy.UserSettings.product_presets_qu_id);
$("#stock_due_soon_days").val(Grocy.UserSettings.stock_due_soon_days);

View File

@ -1,4 +1,6 @@
$('#save-task-category-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-task-category-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
$('#save-task-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-task-button').on('click', function(e)
{
e.preventDefault();

View File

@ -1,4 +1,6 @@
$('#save-transfer-button').on('click', function(e)
import { WindowMessageBag } from '../helpers/messagebag';
$('#save-transfer-button').on('click', function(e)
{
e.preventDefault();
@ -56,11 +58,11 @@
if (productDetails.product.enable_tare_weight_handling == 1)
{
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', 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, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', 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, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
}
else
{
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="Grocy.UndoStockTransaction(\'' + bookingResponse[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
}
if (GetUriParam("embedded") !== undefined)
@ -440,33 +442,6 @@ $("#use_specific_stock_entry").on("change", function()
Grocy.FrontendHelpers.ValidateForm("transfer-form");
});
function UndoStockBooking(bookingId)
{
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Booking successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
function UndoStockTransaction(transactionId)
{
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', {},
function(result)
{
toastr.success(__t("Transaction successfully undone"));
},
function(xhr)
{
console.error(xhr);
}
);
};
if (GetUriParam("embedded") !== undefined)
{

Some files were not shown because too many files have changed in this diff Show More