Compare commits

...

44 Commits

Author SHA1 Message Date
Bernd Bestel
6202e8bda7 Update dependencies for next release 2018-10-06 11:48:47 +02:00
Bernd Bestel
9984e8f218 Small visual/space improvements/changes on recipes and equipment page 2018-10-06 11:43:46 +02:00
Marius Boro
b0c91f6ad1 added missing and updated for new features. (#83) 2018-10-06 09:15:54 +02:00
Marius Boro
9e24586190 Update no.php (#82) 2018-10-06 09:15:40 +02:00
Bernd Bestel
7ba6fc875b Improve responsive embeds (references #25) 2018-10-03 19:05:00 +02:00
Bernd Bestel
3b10906e78 Optimize space around the embedded PDF a little bit (references #25) 2018-10-03 18:04:46 +02:00
Bernd Bestel
ebd24bf30e Use new editor also for recipes 2018-10-03 16:41:21 +02:00
Bernd Bestel
ebd9b1b851 Add possibility to show equipment notes/instruction manuals also in fullscreen mode (references #25) 2018-10-03 16:40:40 +02:00
Bernd Bestel
b242a5de52 Finish equipment / instruction manuals feature (references #25) 2018-10-03 16:11:39 +02:00
Bernd Bestel
81ec011095 Product edit page: Slightly improved styling of barcodes 2018-10-03 13:34:38 +02:00
Bernd Bestel
2a371cc081 Product edit page: Enforce a quantity unit conversion factor > 1 when quantity unit purchase != quantity unit stock 2018-10-03 13:27:36 +02:00
Bernd Bestel
edb986ce24 Added a quick mockup for equipment / instruction manuals (references #25) 2018-10-02 20:03:08 +02:00
Bernd Bestel
f90faca62e Accept only files for product picture file input 2018-10-02 18:33:16 +02:00
Bernd Bestel
6090ac621e Prevent deletion of products with current stock (closes #81) 2018-10-02 18:17:26 +02:00
Bernd Bestel
ae58606d04 Center title in product picture dialog 2018-10-02 18:00:52 +02:00
Bernd Bestel
bb9caf9cc9 Fixed volatil stock logic (fixes #69) 2018-10-02 17:06:21 +02:00
Bernd Bestel
9dd57decdf Finish "pictures for products" features (now closes #58) 2018-10-02 16:48:39 +02:00
Bernd Bestel
f1fc0ee549 Finished first version of "pictures for products" (references #58) 2018-10-01 20:20:50 +02:00
Bernd Bestel
fcdeb33426 Merge branch 'master' of https://github.com/berrnd/grocy 2018-09-30 23:22:45 +02:00
Bernd Bestel
44cd26ae77 Finish first early version of "pictures for products" (references #58) 2018-09-30 23:22:17 +02:00
Marius Boro
04f34ea6b0 Update no.php (#80) 2018-09-30 22:17:15 +02:00
Bernd Bestel
e5fb609c8e Finalize file API (references #58) 2018-09-30 22:16:33 +02:00
Bernd Bestel
c675b534ef Fix missing german translation 2018-09-30 21:41:30 +02:00
Marius Boro
6c74881f95 Update no.php (#79) 2018-09-30 21:40:26 +02:00
Bernd Bestel
756ec319cc Update dependencies for next release 2018-09-30 20:11:24 +02:00
Bernd Bestel
ba2d32be60 Fixes for auto night mode (references #71) 2018-09-30 19:31:03 +02:00
Bernd Bestel
7cc09cec67 The last fix (maybe) for auto night mode handling (references #71) 2018-09-30 18:07:28 +02:00
Bernd Bestel
8b815fce93 Finalize auto night mode feature (references #71) 2018-09-30 18:02:59 +02:00
Bernd Bestel
f1c78659be Optimize user settings 2018-09-30 17:14:04 +02:00
Bernd Bestel
5c79a80f7a Prepared auto night mode configuration option (references #71) 2018-09-30 13:33:21 +02:00
Bernd Bestel
f451e65278 Also log missing localization found in frontend (only when MODE == dev) 2018-09-30 13:02:07 +02:00
Bernd Bestel
176333df5b Save night mode enabled state and apply night mode class to <body> on server side (references #71) 2018-09-30 11:25:07 +02:00
Bernd Bestel
d4227d2e41 Make auto reloading the page on external database changes configurable (closes #74) 2018-09-30 11:17:28 +02:00
Bernd Bestel
0bbd2d9880 Prepare user settings API (references #74 and #71) 2018-09-30 10:47:56 +02:00
Bernd Bestel
b81316bd60 Include products which are not in stock currently but below min. stock amount on stock overview page (fixes #69) 2018-09-30 10:13:37 +02:00
Bernd Bestel
d11dcb38fe Only reload the page on external changes when there is no unsaved form data (fixes #73) 2018-09-30 09:57:42 +02:00
Bernd Bestel
77d82f22dc Fixed scrolling did not work when showing a recipe in fullscreen mode (fixes #76) 2018-09-30 09:41:22 +02:00
Talmai Oliveira
be326a5211 Grocy docker patch (#78)
* typo corrections

* more typos

* initial work towards dockerized version of grocy

* placeholder for future README

* fully working dockerized grocy

* updated final size of docker images
2018-09-30 09:31:16 +02:00
Marius Boro
83624eaf27 Update no.php (#75) 2018-09-30 09:22:30 +02:00
Bernd Bestel
055619d275 Tweaked grocy_night_mode.css slightly (references #71) 2018-09-29 16:35:17 +02:00
Bernd Bestel
cda3dde120 Quick test implementation of night (references #71) 2018-09-29 15:39:16 +02:00
Bernd Bestel
5a0b862d22 v1.19.2 release 2018-09-29 13:45:50 +02:00
Bernd Bestel
bb5fd8360b Fix double form submit when using ENTER (fixes #72) 2018-09-29 13:41:56 +02:00
Marius Boro
d7180bd7b2 Updated no.php (#70)
* Update no.php

* Update no.php
2018-09-27 20:39:11 +02:00
90 changed files with 2341 additions and 190 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.vscode
.gitignore
build.bat
Dockerfile
.DS_store

58
Dockerfile-grocy Normal file
View File

@@ -0,0 +1,58 @@
FROM php:7.2-fpm-alpine
MAINTAINER Talmai Oliveira <to@talm.ai>
RUN apk update && \
apk upgrade && \
apk add --update yarn git &&\
mkdir -p /www && \
# Set environments
sed -i "s|;*daemonize\s*=\s*yes|daemonize = no|g" /usr/local/etc/php-fpm.conf && \
sed -i "s|;*listen\s*=\s*127.0.0.1:9000|listen = 9000|g" /usr/local/etc/php-fpm.conf && \
sed -i "s|;*listen\s*=\s*/||g" /usr/local/etc/php-fpm.conf && \
# sed -i "s|;*log_level\s*=\s*notice|log_level = debug|g" /usr/local/etc/php-fpm.conf && \
sed -i "s|;*chdir\s*=\s*/var/www|chdir = /www|g" /usr/local/etc/php-fpm.d/www.conf && \
# sed -i "s|;*access.log\s*=\s*log/\$pool.access.log|access.log = \$pool.access.log|g" /usr/local/etc/php-fpm.d/www.conf && \
# sed -i "s|;*pm.status_path\s*=\s*/status|pm.status_path = /status|g" /usr/local/etc/php-fpm.d/www.conf && \
# sed -i "s|;*memory_limit =.*|memory_limit = ${PHP_MEMORY_LIMIT}|i" /usr/local/etc/php.ini && \
# sed -i "s|;*upload_max_filesize =.*|upload_max_filesize = ${MAX_UPLOAD}|i" /usr/local/etc/php.ini && \
# sed -i "s|;*max_file_uploads =.*|max_file_uploads = ${PHP_MAX_FILE_UPLOAD}|i" /usr/local/etc/php.ini && \
# sed -i "s|;*post_max_size =.*|post_max_size = ${PHP_MAX_POST}|i" /usr/local/etc/php.ini && \
# sed -i "s|;*cgi.fix_pathinfo=.*|cgi.fix_pathinfo= 0|i" /usr/local/etc/php.ini && \
wget https://raw.githubusercontent.com/composer/getcomposer.org/1b137f8bf6db3e79a38a5bc45324414a6b1f9df2/web/installer -O - -q | php -- --quiet && \
# Cleaning up
rm -rf /var/cache/apk/*
COPY public /www/public
COPY info.php /www/public
COPY controllers /www/controllers
COPY data /www/data
COPY helpers /www/helpers
COPY localization/ /www/localization
COPY middleware/ /www/middleware
COPY migrations/ /www/migrations
COPY publication_assets/ /www/publication_assets
COPY services/ /www/services
COPY views/ /www/views
COPY .yarnrc /www/
COPY *.php /www/
COPY *.json /www/
COPY composer.* /root/.composer/
COPY *yarn* /www/
COPY *.sh /www/
# run php composer.phar with -vvv for extra debug information
RUN cd /var/www/html && \
php composer.phar --working-dir=/www/ -n install && \
cp /www/config-dist.php /www/data/config.php && \
cd /www && \
yarn install && \
chown www-data:www-data -R /www/
# Set Workdir
WORKDIR /www/public
# Expose volumes
VOLUME ["/www"]
# Expose ports
EXPOSE 9000

32
Dockerfile-grocy-nginx Normal file
View File

@@ -0,0 +1,32 @@
FROM alpine:latest
MAINTAINER Talmai Oliveira <to@talm.ai>
RUN apk update && \
apk upgrade && \
apk add --update openssl nginx && \
mkdir -p /etc/nginx/certificates && \
mkdir -p /var/run/nginx && \
mkdir -p /usr/share/nginx/html && \
openssl req \
-x509 \
-newkey rsa:2048 \
-keyout /etc/nginx/certificates/key.pem \
-out /etc/nginx/certificates/cert.pem \
-days 365 \
-nodes \
-subj /CN=localhost && \
rm -rf /var/cache/apk/*
COPY docker_nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker_nginx/common.conf /etc/nginx/common.conf
COPY docker_nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY docker_nginx/conf.d/ssl.conf /etc/nginx/conf.d/ssl.conf
# Expose volumes
VOLUME ["/etc/nginx/conf.d", "/var/log/nginx"]
# Expose ports
EXPOSE 80 443
# Entry point
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]

View File

@@ -6,7 +6,7 @@ ERP beyond your fridge
- Public demo of the latest pre-release version (current master branch) &rarr; [https://demo-prerelease.grocy.info](https://demo-prerelease.grocy.info)
## Motivation
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. ERP your fridge!
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge!
## How to install
> **NEW**
@@ -23,6 +23,16 @@ If you use nginx as your webserver, please include `try_files $uri /index.php;`
If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php` (`Setting('DISABLE_URL_REWRITING', true);`).
## How to run using Docker
The docker images build are based on [Alpine](https://hub.docker.com/_/alpine/), with an extremelly low footprint (less than 10 MB for nginx, and less than 70MB for grocy with php-fm. That number is eventually bumped up to 353MB after all the dependencies are downloaded, however). Anyhow, to run using docker just do the following:
```
> docker-compose up
```
And grocy should be accessible via `http(s)://localhost/`. The https option will work. However, since the certificate is self-signed, most browsers will complain.
## How to update
Just overwrite everything with the latest release while keeping the `data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings). Just to be sure, please empty `data/viewcache`.

74
composer.lock generated
View File

@@ -159,16 +159,16 @@
},
{
"name": "illuminate/container",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
"reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2"
"reference": "2582a994f2f8a153a4880de757a89ad4eeb083d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/container/zipball/0fc33b14ae6cf9a1e694fd43f2a274e590a824b2",
"reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2",
"url": "https://api.github.com/repos/illuminate/container/zipball/2582a994f2f8a153a4880de757a89ad4eeb083d7",
"reference": "2582a994f2f8a153a4880de757a89ad4eeb083d7",
"shasum": ""
},
"require": {
@@ -199,20 +199,20 @@
],
"description": "The Illuminate Container package.",
"homepage": "https://laravel.com",
"time": "2018-05-28T08:50:10+00:00"
"time": "2018-10-03T15:20:19+00:00"
},
{
"name": "illuminate/contracts",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b"
"reference": "9532d673de305b0c0028c0ce60c8952b807d7bc3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/2daf3c078610f744e2a4dc2f44fb5060cce9835b",
"reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/9532d673de305b0c0028c0ce60c8952b807d7bc3",
"reference": "9532d673de305b0c0028c0ce60c8952b807d7bc3",
"shasum": ""
},
"require": {
@@ -243,11 +243,11 @@
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"time": "2018-09-18T12:50:05+00:00"
"time": "2018-10-03T14:04:39+00:00"
},
{
"name": "illuminate/events",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@@ -292,7 +292,7 @@
},
{
"name": "illuminate/filesystem",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
@@ -344,16 +344,16 @@
},
{
"name": "illuminate/support",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742"
"reference": "c7583db6703a36b7fa76254073046e0a920ed276"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/f7c68e8c8aab200cc8ad84f974d5511cda58a742",
"reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742",
"url": "https://api.github.com/repos/illuminate/support/zipball/c7583db6703a36b7fa76254073046e0a920ed276",
"reference": "c7583db6703a36b7fa76254073046e0a920ed276",
"shasum": ""
},
"require": {
@@ -399,20 +399,20 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2018-09-19T18:36:57+00:00"
"time": "2018-10-04T13:27:30+00:00"
},
{
"name": "illuminate/view",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
"reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af"
"reference": "86b8c60e502286135d9c91b0836a58445c4998b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/view/zipball/3ccd29550afe61eb02ad9e4bae0c2e661aadd7af",
"reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af",
"url": "https://api.github.com/repos/illuminate/view/zipball/86b8c60e502286135d9c91b0836a58445c4998b5",
"reference": "86b8c60e502286135d9c91b0836a58445c4998b5",
"shasum": ""
},
"require": {
@@ -447,7 +447,7 @@
],
"description": "The Illuminate View package.",
"homepage": "https://laravel.com",
"time": "2018-09-18T12:50:05+00:00"
"time": "2018-10-02T13:51:18+00:00"
},
{
"name": "morris/lessql",
@@ -1167,16 +1167,16 @@
},
{
"name": "symfony/debug",
"version": "v4.1.4",
"version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "47ead688f1f2877f3f14219670f52e4722ee7052"
"reference": "e3f76ce6198f81994e019bb2b4e533e9de1b9b90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/47ead688f1f2877f3f14219670f52e4722ee7052",
"reference": "47ead688f1f2877f3f14219670f52e4722ee7052",
"url": "https://api.github.com/repos/symfony/debug/zipball/e3f76ce6198f81994e019bb2b4e533e9de1b9b90",
"reference": "e3f76ce6198f81994e019bb2b4e533e9de1b9b90",
"shasum": ""
},
"require": {
@@ -1219,20 +1219,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-08-03T11:13:38+00:00"
"time": "2018-10-02T16:36:10+00:00"
},
{
"name": "symfony/finder",
"version": "v4.1.4",
"version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068"
"reference": "1f17195b44543017a9c9b2d437c670627e96ad06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068",
"reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068",
"url": "https://api.github.com/repos/symfony/finder/zipball/1f17195b44543017a9c9b2d437c670627e96ad06",
"reference": "1f17195b44543017a9c9b2d437c670627e96ad06",
"shasum": ""
},
"require": {
@@ -1268,7 +1268,7 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2018-07-26T11:24:31+00:00"
"time": "2018-10-03T08:47:56+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@@ -1331,16 +1331,16 @@
},
{
"name": "symfony/translation",
"version": "v4.1.4",
"version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f"
"reference": "9f0b61e339160a466ebcde167a6c5521c810e304"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/fa2182669f7983b7aa5f1a770d053f79f0ef144f",
"reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f",
"url": "https://api.github.com/repos/symfony/translation/zipball/9f0b61e339160a466ebcde167a6c5521c810e304",
"reference": "9f0b61e339160a466ebcde167a6c5521c810e304",
"shasum": ""
},
"require": {
@@ -1396,7 +1396,7 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-08-07T12:45:11+00:00"
"time": "2018-10-02T16:36:10+00:00"
},
{
"name": "tuupola/callable-handler",

View File

@@ -7,7 +7,7 @@ Setting('MODE', 'production');
# one of the other available localization files in the "/localization" directory
Setting('CULTURE', 'en');
# To keep it simpel, grocy does not handle any currency conversions,
# To keep it simple: grocy does not handle any currency conversions,
# this here is used to format all money values,
# so can be anything (e. g. "USD" OR "$", doesn't matter...)
Setting('CURRENCY', '$');
@@ -25,3 +25,20 @@ Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
# If, however, your webserver does not support URL rewriting,
# set this to true
Setting('DISABLE_URL_REWRITING', false);
# Default user settings
# These settings can be changed per user, here the defaults
# are defined which are used when the user has not changed the setting so far
# Night mode related
DefaultUserSetting('night_mode_enabled', false); // If night mode is enabled always
DefaultUserSetting('auto_night_mode_enabled', false); // If night mode is enabled automatically when inside a given time range (see the two settings below)
DefaultUserSetting('auto_night_mode_time_range_from', "20:00"); // Format HH:mm
DefaultUserSetting('auto_night_mode_time_range_to', "07:00"); // Format HH:mm
DefaultUserSetting('auto_night_mode_time_range_goes_over_midnight', true); // If the time range above goes over midnight
DefaultUserSetting('currently_inside_night_mode_range', false); // If we're currently inside of night mode time range (this is not user configurable, but stored as a user setting because it's evaluated client side to be able to use the client time instead of the maybe different server time)
# If the page should be automatically reloaded when there was
# an external change
DefaultUserSetting('auto_reload_on_db_change', true);

View File

@@ -5,6 +5,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\DatabaseService;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\LocalizationService;
use \Grocy\Services\UsersService;
class BaseController
{
@@ -41,6 +42,15 @@ class BaseController
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
});
try {
$usersService = new UsersService();
$container->view->set('userSettings', $usersService->GetUserSettings(GROCY_USER_ID));
}
catch (\Exception $ex)
{
// Happens when database is not initialised or migrated...
}
$this->AppContainer = $container;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Grocy\Controllers;
class EquipmentController extends BaseController
{
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'equipment', [
'equipment' => $this->Database->equipment()->orderBy('name')
]);
}
public function EditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['equipmentId'] == 'new')
{
return $this->AppContainer->view->render($response, 'equipmentform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'equipmentform', [
'equipment' => $this->Database->equipment($args['equipmentId']),
'mode' => 'edit'
]);
}
}
}

View File

@@ -14,7 +14,7 @@ class FilesApiController extends BaseApiController
protected $FilesService;
public function Upload(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
public function UploadFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
@@ -29,6 +29,67 @@ class FilesApiController extends BaseApiController
$data = $request->getBody()->getContents();
file_put_contents($this->FilesService->GetFilePath($args['group'], $fileName), $data);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function ServeFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name']))
{
$fileName = $request->getQueryParams()['file_name'];
}
else
{
throw new \Exception('file_name query parameter missing or contains an invalid filename');
}
$filePath = $this->FilesService->GetFilePath($args['group'], $fileName);
if (file_exists($filePath))
{
$response->write(file_get_contents($filePath));
$response = $response->withHeader('Content-Type', mime_content_type($filePath));
return $response->withHeader('Content-Disposition', 'inline; filename="' . $fileName . '"');
}
else
{
return $this->VoidApiActionResponse($response, false, 404, 'File not found');
}
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function DeleteFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name']))
{
$fileName = $request->getQueryParams()['file_name'];
}
else
{
throw new \Exception('file_name query parameter missing or contains an invalid filename');
}
$filePath = $this->FilesService->GetFilePath($args['group'], $fileName);
if (file_exists($filePath))
{
unlink($filePath);
}
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{

View File

@@ -126,7 +126,7 @@ class StockApiController extends BaseApiController
$nextXDays = $request->getQueryParams()['expiring_days'];
}
$expiringProducts = $this->StockService->GetExpiringProducts($nextXDays);
$expiringProducts = $this->StockService->GetExpiringProducts($nextXDays, true);
$expiredProducts = $this->StockService->GetExpiringProducts(-1);
$missingProducts = $this->StockService->GetMissingProducts();
return $this->ApiResponse(array(

View File

@@ -20,4 +20,22 @@ class SystemApiController extends BaseApiController
'changed_time' => $this->DatabaseService->GetDbChangedTime()
));
}
public function LogMissingLocalization(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if (GROCY_MODE === 'dev')
{
try
{
$requestBody = $request->getParsedBody();
$this->LocalizationService->LogMissingLocalization(GROCY_CULTURE, $requestBody['text']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}
}

View File

@@ -68,4 +68,32 @@ class UsersApiController extends BaseApiController
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function GetUserSetting(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$value = $this->UsersService->GetUserSetting(GROCY_USER_ID, $args['settingKey']);
return $this->ApiResponse(array('value' => $value));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function SetUserSetting(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$requestBody = $request->getParsedBody();
$value = $this->UsersService->SetUserSetting(GROCY_USER_ID, $args['settingKey'], $requestBody['value']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
# Usage:
# docker-compose build && docker-compose up
version: '2'
services:
grocy-nginx:
build:
context: .
dockerfile: Dockerfile-grocy-nginx
depends_on:
- grocy
ports:
- '80:80'
- '443:443'
volumes_from:
- grocy
container_name: grocy-nginx
grocy:
build:
context: .
dockerfile: Dockerfile-grocy
expose:
- 9000
environment:
PHP_MEMORY_LIMIT: 512M
MAX_UPLOAD: 50M
PHP_MAX_FILE_UPLOAD: 200
PHP_MAX_POST: 100M
container_name: grocy

28
docker_nginx/common.conf Normal file
View File

@@ -0,0 +1,28 @@
index index.php index.html index.htm;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
expires 365d;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~ \.php$ {
fastcgi_pass grocy:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}

View File

@@ -0,0 +1,8 @@
server {
listen 80 default_server;
server_name _;
root /www/public; # see: volumes_from
include /etc/nginx/common.conf;
}

View File

@@ -0,0 +1,20 @@
server {
listen 443 ssl;
server_name _;
root /www/public; # see: volumes_from
ssl_certificate /etc/nginx/certificates/cert.pem;
ssl_certificate_key /etc/nginx/certificates/key.pem;
error_log /var/log/nginx/error.log;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
include /etc/nginx/common.conf;
}

42
docker_nginx/nginx.conf Normal file
View File

@@ -0,0 +1,42 @@
user nobody;
worker_processes 1;
pid /var/run/nginx/nginx.pid;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
sendfile on;
#tcp_nopush on;
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 50M;
large_client_header_buffers 2 1k;
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/x-javascript text/xml text/css application/xml;
access_log on;
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -44,6 +44,47 @@
}
}
},
"/system/log-missing-localization": {
"post": {
"description": "Logs a missing localization string (only when MODE == 'dev', so should only be called then)",
"tags": [
"System"
],
"requestBody": {
"description": "A valid MissingLocalizationRequest object",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MissingLocalizationRequest"
}
}
}
},
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/get-objects/{entity}": {
"get": {
"description": "Returns all objects of the given entity",
@@ -390,9 +431,58 @@
}
}
},
"/files/upload/{group}": {
"post": {
"description": "Uploads a single file to /data/storage/{group}/{file_name}",
"/file/{group}": {
"get": {
"description": "Serves the given file (with proper Content-Type header)",
"tags": [
"Files"
],
"parameters": [
{
"in": "path",
"name": "group",
"required": true,
"description": "The file group",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "file_name",
"required": true,
"description": "The file name (including extension)",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The binary file contents (Content-Type header is automatically set based on the file type)",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
},
"put": {
"description": "Uploads a single file to /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)",
"tags": [
"Files"
],
@@ -448,6 +538,54 @@
}
}
}
},
"delete": {
"description": "Deletes the given file",
"tags": [
"Files"
],
"parameters": [
{
"in": "path",
"name": "group",
"required": true,
"description": "The file group",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "file_name",
"required": true,
"description": "The file name (including extension)",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/users/get": {
@@ -617,6 +755,97 @@
}
}
},
"/user/settings/{settingKey}": {
"get": {
"description": "Gets the given setting of the currently logged on user",
"tags": [
"User settings"
],
"parameters": [
{
"in": "path",
"name": "settingKey",
"required": true,
"description": "The key of the user setting",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "A UserSetting object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserSetting"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
},
"post": {
"description": "Sets the given setting of the currently logged on user",
"tags": [
"User settings"
],
"requestBody": {
"description": "A valid UserSetting object",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserSetting"
}
}
}
},
"parameters": [
{
"in": "path",
"name": "settingKey",
"required": true,
"description": "The key of the user setting",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/stock/add-product/{productId}/{amount}": {
"get": {
"description": "Adds the the given amount of the given product to stock",
@@ -1448,7 +1677,8 @@
"recipes_pos",
"tasks",
"task_categories",
"product_groups"
"product_groups",
"equipment"
]
},
"StockTransactionType": {
@@ -1500,6 +1730,9 @@
"minimum": 0,
"default": 0
},
"picture_file_name": {
"type": "string"
},
"row_created_timestamp": {
"type": "string",
"format": "date-time"
@@ -2098,6 +2331,22 @@
"format": "date-time"
}
}
},
"UserSetting": {
"type": "object",
"properties": {
"value": {
"type": "string"
}
}
},
"MissingLocalizationRequest": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
}
}
},
"examples": {

View File

@@ -145,6 +145,17 @@ function Setting(string $name, $value)
}
}
global $GROCY_DEFAULT_USER_SETTINGS;
$GROCY_DEFAULT_USER_SETTINGS = array();
function DefaultUserSetting(string $name, $value)
{
global $GROCY_DEFAULT_USER_SETTINGS;
if (!array_key_exists($name, $GROCY_DEFAULT_USER_SETTINGS))
{
$GROCY_DEFAULT_USER_SETTINGS[$name] = $value;
}
}
function GetUserDisplayName($user)
{
$displayName = '';
@@ -181,7 +192,7 @@ function Pluralize($number, $singularForm, $pluralForm)
function IsValidFileName($fileName)
{
if(preg_match('#^[a-z0-9]+\.[a-z]+?$#i', $fileName))
if(preg_match('=^[^/?*;:{}\\\\]+\.[^/?*;:{}\\\\]+$=', $fileName))
{
return true;
}

6
info.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
// Show all information, defaults to INFO_ALL
phpinfo();
?>

View File

@@ -249,6 +249,34 @@ return array(
'Already expired' => 'Bereits abgelaufen',
'Due soon' => 'Bald fällig',
'Overdue' => 'Überfällig',
'View settings' => 'Ansichtseinstellungen',
'Auto reload on external changes' => 'Autom. akt. bei externen Änderungen',
'Enable night mode' => 'Nachtmodus aktivieren',
'Auto enable in time range' => 'Autom. akt. in diesem Zeitraum',
'From' => 'Von',
'in format' => 'im Format',
'To' => 'Bis',
'Time range goes over midnight' => 'Zeitraum geht über Mitternacht',
'Product picture' => 'Produktbild',
'No file selected' => 'Keine Datei ausgewählt',
'If you don\'t select a file, the current picture will not be altered' => 'Wenn du keine Datei auswählst, wird das aktuelle Bild nicht verändert',
'Current picture' => 'Aktuelles Bild',
'Delete' => 'Löschen',
'The current picture will be deleted when you save the product' => 'Das aktuelle Bild wird beim Speichern des Produkts gelöscht',
'Select file' => 'Datei auswählen',
'Image of product #1' => 'Bild des Produkts #1',
'This product cannot be deleted because it is in stock, please remove the stock amount first.' => 'Dieses Produkt kann nicht gelöscht werden, da es auf Lager ist, bitte zuerst den Bestand entfernen.',
'Delete not possible' => 'Löschen nicht möglich',
'Equipment' => 'Ausstattung',
'Instruction manual' => 'Bedienungsanleitung',
'The selected equipment has no instruction manual' => 'Das ausgewählte Gerät hat keine Bedienungsanleitung',
'Notes' => 'Notizen',
'Edit equipment' => 'Geräte bearbeiten',
'Create equipment' => 'Geräte erstellen',
'If you don\'t select a file, the current instruction manual will not be altered' => 'Wenn du keine Datei auswählst, wird die aktuelle Bedienungsanleitung nicht verändert',
'Current instruction manual' => 'Aktuelle Bedienungsanleitung',
'No instruction manual available' => 'Keine Bedienungsanleitung vorhanden',
'The current instruction manual will be deleted when you save the equipment' => 'Die aktuelle Bedienungsanleitung wird beim Speichern des Geräts gelöscht',
//Constants
'manually' => 'Manuell',
@@ -259,6 +287,7 @@ return array(
'timeago_nan' => 'vor NaN Jahren',
'moment_locale' => 'de',
'datatables_localization' => '{"sEmptyTable":"Keine Daten in der Tabelle vorhanden","sInfo":"_START_ bis _END_ von _TOTAL_ Einträgen","sInfoEmpty":"Keine Daten vorhanden","sInfoFiltered":"(gefiltert von _MAX_ Einträgen)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ Einträge anzeigen","sLoadingRecords":"Wird geladen ..","sProcessing":"Bitte warten ..","sSearch":"Suchen","sZeroRecords":"Keine Einträge vorhanden","oPaginate":{"sFirst":"Erste","sPrevious":"Zurück","sNext":"Nächste","sLast":"Letzte"},"oAria":{"sSortAscending":": aktivieren, um Spalte aufsteigend zu sortieren","sSortDescending":": aktivieren, um Spalte absteigend zu sortieren"},"select":{"rows":{"0":"Zum Auswählen auf eine Zeile klicken","1":"1 Zeile ausgewählt","_":"%d Zeilen ausgewählt"}},"buttons":{"print":"Drucken","colvis":"Spalten","copy":"Kopieren","copyTitle":"In Zwischenablage kopieren","copyKeys":"Taste <i>ctrl</i> oder <i>⌘</i> + <i>C</i> um Tabelle<br>in Zwischenspeicher zu kopieren.<br><br>Um abzubrechen die Nachricht anklicken oder Escape drücken.","copySuccess":{"1":"1 Spalte kopiert","_":"%d Spalten kopiert"}}}',
'summernote_locale' => 'de-DE',
//Demo data
'Cookies' => 'Cookies',
@@ -330,5 +359,7 @@ return array(
'Tinned food' => 'Konservern',
'Butchery products' => 'Metzgerei',
'Vegetables/Fruits' => 'Obst/Gemüse',
'Refrigerated products' => 'Kühlregal'
'Refrigerated products' => 'Kühlregal',
'Coffee machine' => 'Kaffeemaschine',
'Dishwasher' => 'Spülmaschine'
);

View File

@@ -9,5 +9,6 @@ return array(
'timeago_locale' => 'en',
'timeago_nan' => 'NaN years ago',
'moment_locale' => '',
'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}'
'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}',
'summernote_locale' => ''
);

View File

@@ -150,6 +150,7 @@ return array(
'timeago_nan' => 'NaN anni fa',
'moment_locale' => 'it',
'datatables_localization' => '{"sEmptyTable":"Nessun dato disponibile","sInfo":"Mostrando da _START_ a _END_ di _TOTAL_ voci","sInfoEmpty":"Mostrando da 0 a 0 di 0 voci","sInfoFiltered":"(Filtrato da _MAX_ voci totali)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Mostra _MENU_ voci","sLoadingRecords":"Caricando...","sProcessing":"Calcolando...","sSearch":"Cerca:","sZeroRecords":"Nessun risultato trovato","oPaginate":{"sFirst":"Prima","sLast":"Ultima","sNext":"Prossima","sPrevious":"Precedente"},"oAria":{"sSortAscending":": ordine crescente","sSortDescending":": ordine decrescente"}}',
'summernote_locale' => 'it-IT',
//Demo data
'Cookies' => 'Biscotti',

View File

@@ -9,20 +9,20 @@ return array(
'Amount' => 'Antall',
'Next best before date' => 'Kommende best før dato',
'Logout' => 'Logg ut',
'Chores overview' => 'Oversikt Husoppgaver',
'Chores overview' => 'Oversikt Husarbeid',
'Batteries overview' => 'Oversikt Batteri',
'Purchase' => 'Innkjøp',
'Consume' => 'Forbrukt',
'Consume' => 'Forbruk produkt',
'Inventory' => 'Endre Husholdning',
'Shopping list' => 'Handleliste',
'Chore tracking' => 'Logge Husoppgaver',
'Chore tracking' => 'Logge Husarbeid',
'Battery tracking' => 'Batteri Ladesyklus',
'Products' => 'Produkter',
'Locations' => 'Lokasjoner',
'Quantity units' => 'Forpakning',
'Chores' => 'Husoppgaver',
'Chores' => 'Husarbeid',
'Batteries' => 'Batterier',
'Chore' => 'Husoppgave',
'Chore' => 'Husarbeid',
'Next estimated tracking' => 'Neste handling',
'Last tracked' => 'Sist logget',
'Battery' => 'Batteri',
@@ -41,7 +41,7 @@ return array(
'New amount' => 'Nytt antall',
'Note' => 'Info',
'Tracked time' => 'Tid utført/ ladet',
'Chore overview' => 'Oversikt Husoppgave',
'Chore overview' => 'Oversikt Husarbeid',
'Tracked count' => 'Antall utførelser/ ladninger',
'Battery overview' => 'Batteri Oversikt',
'Charge cycles count' => 'Antall ladesykluser',
@@ -68,11 +68,11 @@ return array(
'Create quantity unit' => 'Opprett forpakning',
'Period type' => 'Gjentakelse',
'Period days' => 'Antall dager for gjentakelse',
'Create chore' => 'Opprett husoppgave',
'Create chore' => 'Opprett husarbeid oppgave',
'Used in' => 'Brukt',
'Create battery' => 'Opprett batteri',
'Edit battery' => 'Endre batteri',
'Edit chore' => 'Endre husoppgave',
'Edit chore' => 'Endre husarbeid oppgave',
'Edit quantity unit' => 'Endre forpakning',
'Edit product' => 'Endre produkt',
'Edit location' => 'Endre lokasjon',
@@ -90,7 +90,7 @@ return array(
'Are you sure to delete battery "#1"?' => 'Er du sikker du ønsker å slette Batteri "#1"?',
'Yes' => 'Ja',
'No' => 'Nei',
'Are you sure to delete chore "#1"?' => 'Er du sikker på du ønsker å slette husoppgave "#1"?',
'Are you sure to delete chore "#1"?' => 'Er du sikker på du ønsker å slette husarbeid oppgave "#1"?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" kunne ikke bli tildelt et produkt, hvordan ønsker du å fortsette?',
'Create or assign product' => 'Opprett eller tildel til produkt',
'Cancel' => 'Avbryt',
@@ -110,29 +110,29 @@ return array(
'This product is not in stock' => 'Dette produktet er ikke i husholdningen',
'This means #1 will be added to stock' => 'Dette betyr at #1 vil bli lagt til i husholdningen',
'This means #1 will be removed from stock' => 'Dette betyr at #1 vil bli fjernet fra husholdningen',
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Dette betyr at det er estimert at den nye utførelsen av denne husoppgaven er logget #1 dag etter den sist var logget',
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Dette betyr at det er estimert at den nye utførelsen av denne husarbeid oppgaven er logget #1 dag etter den sist var logget',
'Removed #1 #2 of #3 from stock' => 'Fjernet #1 #2 #3 fra husholdningen',
'About grocy' => 'Om Grocy',
'Close' => 'Lukk',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batteri må lades innen de #2 neste dagene',
'#1 batteries are overdue to be charged' => '#1 Batteri har gått over fristen for å bli ladet opp',
'#1 chores are due to be done within the next #2 days' => '#1 husoppgaver skal gjøres inne de #2 neste dagene',
'#1 chores are overdue to be done' => '#1 husoppgaver har gått over fristen for utførelse',
'#1 chores are due to be done within the next #2 days' => '#1 husarbeid(s) oppgave(r) skal gjøres inne de #2 neste dagene',
'#1 chores are overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
'Released on' => 'Utgitt',
'Consume #3 #1 of #2' => 'Forbruk #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 lagt til i husholdningen',
'Stock amount of #1 is now #2 #3' => 'Husholdning antall #1 er nå #2 #3',
'Tracked execution of chore #1 on #2' => 'Utførte husoppgave "#1" den #2',
'Tracked execution of chore #1 on #2' => 'Utførte husarbeid oppgave "#1" den #2',
'Tracked charge cycle of battery #1 on #2' => 'Ladet #1 den #2',
'Consume all #1 which are currently in stock' => 'Konsumér alle #1 som er i husholdningen',
'Consume all #1 which are currently in stock' => 'Forbruk alle #1 som er i husholdningen',
'All' => 'Alle',
'Track charge cycle of battery #1' => '#1 ladet',
'Track execution of chore #1' => 'Utfør husoppgave #1',
'Track execution of chore #1' => 'Utfør husarbeid oppgave #1',
'Filter by location' => 'Filtrér etter lokasjon',
'Search' => 'Søk',
'Not logged in' => 'Ikke logget inn',
'You have to select a product' => 'Du må velge et produkt',
'You have to select a chore' => 'Du må velge en husoppgaven',
'You have to select a chore' => 'Du må velge en husarbeid oppgave',
'You have to select a battery' => 'Du må velge et batteri',
'A name is required' => 'Vennligst fyll inn et navn',
'A location is required' => 'En lokasjon kreves',
@@ -183,8 +183,8 @@ return array(
'Done by' => 'Utført av',
'Last done by' => 'Sist utført av',
'Unknown' => 'Ukjent',
'Filter by chore' => 'Filtrér husoppave',
'Chores analysis' => 'Statistikk husoppgaver',
'Filter by chore' => 'Filtrér husarbeid',
'Chores analysis' => 'Statistikk husarbeid',
'0 means suggestions for the next charge cycle are disabled' => '0 betyr neste ladesyklus er avslått',
'Charge cycle interval (days)' => 'Ladesyklysintervall (Dager)',
'Last price' => 'Siste pris',
@@ -198,8 +198,8 @@ return array(
'#1 product is below defined min. stock amount' => '#1 Produkt er under minimums husholdningsnivå',
'Unit' => 'Enhet',
'Units' => 'Enheter',
'#1 chore is due to be done within the next #2 days' => '#1 husoppgave skal gjøres inne de #2 neste dagene',
'#1 chore is overdue to be done' => '#1 husoppgave har gått over fristen for utførelse',
'#1 chore is due to be done within the next #2 days' => '#1 husarbeid oppgave(r) skal gjøres inne de #2 neste dagene',
'#1 chore is overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
'#1 battery is due to be charged within the next #2 days' => '#1 Batteri må lades innen #2 dager',
'#1 battery is overdue to be charged' => '#1 Batteri har gått over fristen for å lades',
'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 enhet ble automatisk lagt til i tillegg til hva som blir skrevet inn her',
@@ -212,8 +212,72 @@ return array(
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Huk av hvis du ønsker å bruke mindre enn forpakningsstørrelse i husholdningen',
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "bruke mindre enn forpakningsstørrelse i husholdningen" blir ignorert',
'Removed all ingredients of recipe "#1" from stock' => 'Fjern alle ingredienser for "#1" oppskriften fra husholdningen.',
'Consume all ingredients needed by this recipe' => 'Konsumer alle ingredienser for denne oppskriften',
'Consume all ingredients needed by this recipe' => 'Forbruk alle ingredienser for denne oppskriften',
'Click to show technical details' => 'Klikk for å vise teknisk informasjon',
'Error while saving, probably this item already exists' => 'Kunne ikke lagre, produkt er lagt til fra før',
'Error details' => 'Detaljer om feil',
'Tasks' => 'Oppgaver',
'Show done tasks' => 'Vis ferdige oppgaver',
'Task' => 'Oppgave',
'Due' => 'Forfall',
'Assigned to' => 'Tildelt',
'Mark task "#1" as completed' => 'Merk oppgave "#1" som ferdig',
'Uncategorized' => 'Mangler kategori',
'Task categories' => 'Oppgave kategorier',
'Create task' => 'Opprett en oppgave',
'A due date is required' => 'En forfallsdato kreves',
'Category' => 'Kategori',
'Edit task' => 'Endre oppgave',
'Are you sure to delete task "#1"?' => 'Er du sikker du ønsker slette oppgave "#1"?',
'#1 task is due to be done within the next #2 days' => '#1 oppgave har utførelse forfall innen de neste #2 dagene',
'#1 tasks are due to be done within the next #2 days' => '#1 oppgaver har utførelse forfall innen de neste #2 dagene',
'#1 task is overdue to be done' => '#1 oppgave har forfalt utførelse dato',
'#1 tasks are overdue to be done' => '#1 oppgaver har forfalt utførelse dato',
'Edit task category' => 'Endre oppgave kategori',
'Create task category' => 'Opprett oppgave kategori',
'Product groups' => 'Produktgrupper',
'Ungrouped' => 'Ikke i grupper',
'Create product group' => 'Opprett produkt gruppe',
'Edit product group' => 'Endre produkt gruppe',
'Product group' => 'Produktgruppe',
'Are you sure to delete product group "#1"?' => 'Er du sikker du ønsker å slette produktgruppe "#1"?',
'Stay logged in permanently' => 'Alltid være innlogget',
'When not set, you will get logged out at latest after 30 days' => 'Når den ikke er satt vil du bli logget ut etter 30 dager',
'Filter by status' => 'Filtrér etter status',
'Below min. stock amount' => 'Under under minimum husholdningsnivå',
'Expiring soon' => 'Går snart ut på dato',
'Already expired' => 'Utgått på dato',
'Due soon' => 'Forfaller snart',
'Overdue' => 'Forfalt',
'View settings' => 'Se instillinger',
'Auto reload on external changes' => 'Automatisk fornying ved ekstern endring',
'Enable night mode' => 'Aktiver nattmodus',
'Auto enable in time range' => 'Automatisk aktivering i tidsrommet',
'From' => 'Fra',
'in format' => 'format',
'To' => 'Til',
'Time range goes over midnight' => 'Tidsrommet går over midnatt',
'Product picture' => 'Produktbilde',
'No file selected' => 'Ingen fil merket',
'If you don\'t select a file, the current picture will not be altered' => 'Hvis du ikke velger et bilde, vil nåværende bilde ikke bli endret',
'Current picture' => 'Nåværende bilde',
'Delete' => 'Slett',
'The current picture will be deleted when you save the product' => 'Nåværende bilde vil bli slettet når du lagrer produktet',
'Select file' => 'Velg fil',
'Image of product #1' => 'Bilde av produkt #1',
'This product cannot be deleted because it is in stock, please remove the stock amount first.' => 'Dette produktet kan ikke slettes fordi det er gjenværende produkter i husholdningen',
'Delete not possible' => 'Ikke mulig å slette',
'Equipment' => 'Utstyr',
'Instruction manual' => 'Instruksjonsmanual',
'The selected equipment has no instruction manual' => 'Merket utstyr har ingen instruksjonsmanual',
'Notes' => 'Notater',
'Edit equipment' => 'Endre utstyr',
'Create equipment' => 'Opprett utstyr',
'If you don\'t select a file, the current instruction manual will not be altered' => 'Hvis du ikke velger en instruksjonsmanual, vil nåværende instruksjonsmanual ikke bli endret',
'Current instruction manual' => 'Nåværende instruksjonsmanual',
'No instruction manual available' => 'Ingen instruksjonsmanual tilgjengelig',
'The current instruction manual will be deleted when you save the equipment' => 'Nåværende instruksjonsmanual vil bli slettet når du lagrer utstyret',
//Constants
'manually' => 'Manuel',
'dynamic-regular' => 'Automatisk',
@@ -223,6 +287,7 @@ return array(
'timeago_nan' => 'for NaN År',
'moment_locale' => 'nb',
'datatables_localization' => '{"sEmptyTable":"Det finnes ingen data i tabellen","sInfo":"_START_ fra _END_ til _TOTAL_ skriv","sInfoEmpty":"Ingen data tilgjengelign","sInfoFiltered":"(filtrert fra _MAX_ skriv)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ registrer deg","sLoadingRecords":"Laster ..","sProcessing":"Vennligst vent ..","sSearch":"Søk","sZeroRecords":"Ingen oppføringer tilgjengelig","oPaginate":{"sFirst":"Første","sPrevious":"Bakover","sNext":"Neste","sLast":"Siste"},"oAria":{"sSortAscending":": Sortér stigende","sSortDescending":": Sortér synkende"},"select":{"rows":{"0":"klikk på en linje for å velge","1":"1 linje valgt","_":"%d linger valgt"}},"buttons":{"print":"Print","colvis":"Søyle","copy":"Kopi","copyTitle":"Kopier til utklippstavlen","copyKeys":"Trykk <i>ctrl</i> eller <i>⌘</i> + <i>C</i> for å kopiere tabell<br> til utklipptavlen.<br><br>For å avbryte, klikke på meldingen eller trykk på ESC.","copySuccess":{"1":"1 Kolonne kopiert","_":"%d kolonne kopiert"}}}',
'summernote_locale' => 'nb-NO',
//Demo data
'Cookies' => 'Cookies',
@@ -282,5 +347,19 @@ return array(
'Grams' => 'Gram',
'Flour' => 'Mel',
'Pancakes' => 'Pannekaker',
'Sugar' => 'Sukker'
'Sugar' => 'Sukker',
'Home' => 'Hus',
'Life' => 'Livstil',
'Projects' => 'Projekter',
'Repair the garage door' => 'Reparere garasjedøren',
'Fork and improve grocy' => 'Fork og forbedre grocy',
'Find a solution for what to do when I forget the door keys' => 'Finne på løsning for hva jeg skal gjøre når jeg mister dørnøklene',
'Sweets' => 'Godteri',
'Bakery products' => 'Produkt fra bakeren ',
'Tinned food' => 'Boksemat',
'Butchery products' => 'Produkt fra slakteren',
'Vegetables/Fruits' => 'Frukt/ Grønnsaker',
'Refrigerated products' => 'Kjølte produkter'
'Coffee machine' => 'Kaffetrakter',
'Dishwasher' => 'Oppvaskmaskin'
);

View File

@@ -7,6 +7,14 @@ class JsonMiddleware extends BaseMiddleware
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'application/json');
if ($response->hasHeader('Content-Disposition'))
{
return $response;
}
else
{
return $response->withHeader('Content-Type', 'application/json');
}
}
}

27
migrations/0038.sql Normal file
View File

@@ -0,0 +1,27 @@
DROP VIEW stock_missing_products;
CREATE VIEW stock_missing_products
AS
SELECT
p.id,
MAX(p.name) AS name,
p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing,
CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products p
LEFT JOIN stock s
ON p.id = s.product_id
WHERE p.min_stock_amount != 0
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;
DROP VIEW stock_current;
CREATE VIEW stock_current
AS
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
FROM stock
GROUP BY product_id
UNION
SELECT id, 0, null
FROM stock_missing_products
WHERE is_partly_in_stock = 0;

10
migrations/0039.sql Normal file
View File

@@ -0,0 +1,10 @@
CREATE TABLE user_settings (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
row_updated_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(user_id, key)
);

2
migrations/0040.sql Normal file
View File

@@ -0,0 +1,2 @@
ALTER TABLE products
ADD picture_file_name TEXT;

7
migrations/0041.sql Normal file
View File

@@ -0,0 +1,7 @@
CREATE TABLE equipment (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
instruction_manual_file_name TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

View File

@@ -23,6 +23,7 @@
"jquery-ui-dist": "^1.12.1",
"moment": "^2.22.2",
"startbootstrap-sb-admin": "^4.0.0",
"summernote": "^0.8.10",
"swagger-ui-dist": "^3.17.3",
"tagmanager": "https://github.com/max-favilli/tagmanager.git#3.0.2",
"tempusdominus-bootstrap-4": "^5.0.1",

View File

@@ -50,12 +50,13 @@ a.discrete-link:focus {
}
.fullscreen {
z-index: 9999;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
overflow: auto;
}
.form-check-input.is-valid ~ .form-check-label,
@@ -80,6 +81,12 @@ input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
.centered-dialog .modal-title,
.centered-dialog .modal-body {
margin-left: auto;
margin-right: auto;
}
/* Navigation style customizations */
#mainNav {
background-color: #e5e5e5 !important;

View File

@@ -0,0 +1,221 @@
body.night-mode {
color: #c1c1c1;
}
.night-mode .table-info,
.night-mode .table-info > td,
.night-mode .table-info > th {
background-color: #07373f;
}
.night-mode .btn,
.night-mode .nav-link,
.night-mode #mainNav.navbar-light .navbar-collapse .navbar-nav > .nav-item.dropdown > .nav-link::after,
.night-mode .dropdown-item {
color: #c1c1c1 !important;
}
.night-mode .btn-outline-dark {
border-color: #c1c1c1;
}
.night-mode .btn-info {
color: #c1c1c1;
background-color: #07373f;
border-color: #07373f;
}
.night-mode .btn-warning {
color: #c1c1c1;
background-color: #473604;
border-color: #473604;
}
.night-mode .btn-danger {
color: #c1c1c1;
background-color: #471116;
border-color: #471116;
}
.night-mode .btn-success {
color: #c1c1c1;
background-color: #0d3a18;
border-color: #0d3a18;
}
.night-mode .form-control {
color: #495057;
background-color: #333131;
border: 1px solid #ced4da;
}
.night-mode .content-wrapper {
background: #333131;
}
.night-mode table.dataTable tr.group td {
font-weight: bold;
background-color: #333131;
}
.night-mode .table-danger,
.night-mode .table-danger > td,
.night-mode .table-danger > th {
background-color: #471116;
}
.night-mode .table-warning,
.night-mode .table-warning > td,
.night-mode .table-warning > th {
background-color: #473604;
}
.night-mode .bg-warning {
background-color: #473604!important;
}
.night-mode .bg-info {
background-color: #07373f!important;
}
.night-mode .bg-danger {
background-color: #471116!important;
}
.night-mode .form-control:focus {
color: #495057;
background-color: #333131;
border-color: #80bdff;
}
.night-mode .dropdown-item:focus,
.night-mode .dropdown-item:hover {
color: #16181b;
background-color: #333131;
}
.night-mode .dropdown-item {
color: #7c7b6f;
background-color: #333131;
}
.night-mode .list-group-item {
background-color: #333131;
}
.night-mode .modal-content {
background-color: #1a1919;
border: 1px solid rgba(186, 189, 189, 0.66);
}
.night-mode .modal-footer {
border-top: 1px solid #6f7173;
}
.night-mode .container-fluid {
background-color: #333131;
}
.night-mode a.discrete-link:hover {
color: #16354f !important;
text-decoration: none !important;
background-color: #333131;
}
.night-mode a.discrete-link:focus {
color: #3a0b0f !important;
background-color: #333131;
}
.night-mode .card {
border: 2px solid;
border-color: #383838;
background-color: #333131;
}
.night-mode .card-header {
background-color: #333131;
}
.night-mode #mainNav {
background-color: #333131 !important;
border-bottom: 2px solid !important;
border-color: #383838 !important;
}
.night-mode .navbar-sidenav,
.night-mode .sidenav-second-level {
background-color: #333131 !important;
border-right: 2px solid !important;
border-color: #383838 !important;
}
.night-mode .navbar-nav .dropdown-menu {
background-color: #333131 !important;
}
.night-mode .navbar-nav .dropdown-divider {
border-color: #383838 !important;
background-color: #333131;
}
.night-mode .sidenav-toggler {
background-color: #383838 !important;
border-right: 2px solid !important;
border-color: #383838 !important;
}
.night-mode .navbar-sidenav > li:hover,
.night-mode .sidenav-second-level > li:hover,
.night-mode .navbar-nav .dropdown-item:hover {
box-shadow: inset 5px 0 0 #112a3f !important;
background-color: #383838 !important;
color: #c1c1c1 !important;
}
.night-mode .navbar-sidenav > li > a:focus,
.night-mode .sidenav-second-level > li > a:focus,
.night-mode .navbar-nav .dropdown-item:focus {
box-shadow: inset 5px 0 0 #350a0f !important;
background-color: #383838 !important;
color: #c1c1c1 !important;
}
.night-mode .active-page {
box-shadow: inset 5px 0 0 #350a0f !important;
background-color: #383838 !important;
}
.night-mode .toast-success {
background-color: #092810;
}
.night-mode .toast-error {
background-color: #471015;
}
.night-mode .typeahead .active {
background-color: #333131;
}
.night-mode .note-editor.note-frame .note-editing-area .note-editable {
color: #c1c1c1;
background-color: #333131;
}
.night-mode .bootstrap-datetimepicker-widget table td.day {
background-color: #333131;
}
.night-mode .bootstrap-datetimepicker-widget table td {
background-color: #333131;
}
.night-mode .bootstrap-datetimepicker-widget table td,
.night-mode .bootstrap-datetimepicker-widget table th {
background-color: #333131;
}
.night-mode .dropdown-menu {
background-color: #333131;
}

View File

@@ -41,3 +41,26 @@ IsTouchInputDevice = function()
return false;
}
BoolVal = function(test)
{
var anything = test.toString().toLowerCase();
if (anything === true || anything === "true" || anything === "1" || anything === "on")
{
return true;
}
else
{
return false;
}
}
GetFileNameFromPath = function(path)
{
return path.split("/").pop().split("\\").pop();
}
GetFileExtension = function(pathOrFileName)
{
return pathOrFileName.split(".").pop();
}

View File

@@ -3,6 +3,22 @@
var localizedText = Grocy.LocalizationStrings[text];
if (localizedText === undefined)
{
if (Grocy.Mode === 'dev')
{
jsonData = {};
jsonData.text = text;
Grocy.Api.Post('system/log-missing-localization', jsonData,
function(result)
{
// Nothing to do...
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
localizedText = text;
}
@@ -161,6 +177,68 @@ Grocy.Api.Post = function(apiFunction, jsonData, success, error)
xhr.send(JSON.stringify(jsonData));
};
Grocy.Api.UploadFile = function(file, group, fileName, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName));
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('PUT', url, true);
xhr.setRequestHeader('Content-type', 'application/octet-stream');
xhr.send(file);
};
Grocy.Api.DeleteFile = function(fileName, group, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName));
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('DELETE', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send();
};
Grocy.FrontendHelpers = { };
Grocy.FrontendHelpers.ValidateForm = function(formId)
{
@@ -191,3 +269,75 @@ Grocy.FrontendHelpers.ShowGenericError = function(message, exception)
console.error(exception);
}
$("form").on("keyup paste", "input, textarea", function()
{
$(this).closest("form").addClass("is-dirty");
});
$("form").on("click", "select", function()
{
$(this).closest("form").addClass("is-dirty");
});
// Auto saving user setting controls
$(".user-setting-control").on("change", function()
{
var element = $(this);
var inputType = element.attr("type").toLowerCase();
var settingKey = element.attr("data-setting-key");
if (inputType === "checkbox")
{
value = element.is(":checked");
}
else
{
var value = element.val();
}
Grocy.UserSettings[settingKey] = value;
jsonData = { };
jsonData.value = value;
Grocy.Api.Post('user/settings/' + settingKey, jsonData,
function(result)
{
// Nothing to do...
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
});
// 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: "' + L("Select file") + '"; }').appendTo("head");
}
ResizeResponsiveEmbeds = function(fillEntireViewport = false)
{
if (!fillEntireViewport)
{
var maxHeight = $("body").height() - $("#mainNav").outerHeight() - 62;
}
else
{
var maxHeight = $("body").height();
}
$(".embed-responsive").attr("height", maxHeight.toString() + "px");
}
$(window).on('resize', function()
{
console.log($("body").hasClass("fullscreen-responsive-embed-active"));
ResizeResponsiveEmbeds($("body").hasClass("fullscreen-responsive-embed-active"));
});

View File

@@ -10,7 +10,8 @@
);
// Check if the database has changed once a minute
// If a change is detected, reload the current page, but only if already idling for at least 50 seconds
// 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
setInterval(function()
{
Grocy.Api.Get('system/get-db-changed-time',
@@ -21,7 +22,10 @@ setInterval(function()
{
if (Grocy.IdleTime >= 50)
{
window.location.reload();
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0)
{
window.location.reload();
}
}
Grocy.DatabaseChangedTime = newDbChangedTime;
@@ -51,3 +55,8 @@ setInterval(function()
{
Grocy.IdleTime += 1;
}, 1000);
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change))
{
$("#auto-reload-enabled").prop("checked", true);
}

View File

@@ -0,0 +1,106 @@
$("#night-mode-enabled").on("change", function()
{
var value = $(this).is(":checked");
if (value)
{
$("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(Grocy.UserSettings.night_mode_enabled))
{
$("body").removeClass("night-mode");
}
});
$(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");
}
CheckNightMode();
});
$("#auto-night-mode-time-range-goes-over-midgnight").on("change", function()
{
CheckNightMode();
});
$("#night-mode-enabled").prop("checked", BoolVal(Grocy.UserSettings.night_mode_enabled));
$("#auto-night-mode-enabled").prop("checked", BoolVal(Grocy.UserSettings.auto_night_mode_enabled));
$("#auto-night-mode-time-range-goes-over-midgnight").prop("checked", BoolVal(Grocy.UserSettings.auto_night_mode_time_range_goes_over_midnight));
$("#auto-night-mode-enabled").trigger("change");
$("#auto-night-mode-time-range-from").val(Grocy.UserSettings.auto_night_mode_time_range_from);
$("#auto-night-mode-time-range-from").trigger("keyup");
$("#auto-night-mode-time-range-to").val(Grocy.UserSettings.auto_night_mode_time_range_to);
$("#auto-night-mode-time-range-to").trigger("keyup");
function CheckNightMode()
{
if (!BoolVal(Grocy.UserSettings.auto_night_mode_enabled))
{
return;
}
var start = moment(Grocy.UserSettings.auto_night_mode_time_range_from, "HH:mm", true);
var end = moment(Grocy.UserSettings.auto_night_mode_time_range_to, "HH:mm", true);
var now = moment();
if (!start.isValid() || !end.isValid)
{
return;
}
if (BoolVal(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");
}
}
}
CheckNightMode();
if (Grocy.Mode === "production")
{
setInterval(CheckNightMode, 60000);
}
else
{
setInterval(CheckNightMode, 4000);
}

View File

@@ -39,9 +39,10 @@ $('#battery-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('battery-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -68,9 +68,10 @@ $('#batterytracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('batterytracking-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -39,9 +39,10 @@ $('#chore-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('chore-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -65,9 +65,10 @@ $('#choretracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('choretracking-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -24,6 +24,18 @@ Grocy.Components.ProductCard.Refresh = function(productId)
$('#productcard-product-last-price').text(L('Unknown'));
}
if (productDetails.product.picture_file_name !== null && !productDetails.product.picture_file_name.isEmpty())
{
$("#productcard-no-product-picture").addClass("d-none");
$("#productcard-product-picture").removeClass("d-none");
$("#productcard-product-picture").attr("src", U('/api/file/productpictures?file_name=' + productDetails.product.picture_file_name));
}
else
{
$("#productcard-no-product-picture").removeClass("d-none");
$("#productcard-product-picture").addClass("d-none");
}
EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', L('timeago_nan'));
EmptyElementWhenMatches('#productcard-product-last-used-timeago', L('timeago_nan'));
},

View File

@@ -90,9 +90,10 @@ $('#consume-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('consume-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

128
public/viewjs/equipment.js Normal file
View File

@@ -0,0 +1,128 @@
var equipmentTable = $('#equipment-table').DataTable({
'paginate': false,
'order': [[0, 'asc']],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
},
'select': 'single',
'initComplete': function()
{
this.api().row({ order: 'current' }, 0).select();
DisplayEquipment($('#equipment-table tbody tr:eq(0)').data("equipment-id"));
}
});
equipmentTable.on('select', function(e, dt, type, indexes)
{
if (type === 'row')
{
var selectedEquipmentId = $(equipmentTable.row(indexes[0]).node()).data("equipment-id");
DisplayEquipment(selectedEquipmentId)
}
});
function DisplayEquipment(id)
{
Grocy.Api.Get('get-object/equipment/' + id,
function(equipmentItem)
{
$(".selected-equipment-name").text(equipmentItem.name);
$("#description-tab-content").html(equipmentItem.description);
if (equipmentItem.instruction_manual_file_name !== null && !equipmentItem.instruction_manual_file_name.isEmpty())
{
var pdfUrl = U('/api/file/equipmentmanuals?file_name=' + equipmentItem.instruction_manual_file_name);
$("#selected-equipment-instruction-manual").attr("src", pdfUrl);
$("#selected-equipment-instruction-manual").removeClass("d-none");
$("#selected-equipment-has-no-instruction-manual-hint").addClass("d-none");
$("a[href='#instruction-manual-tab']").tab("show");
ResizeResponsiveEmbeds();
}
else
{
$("#selected-equipment-instruction-manual").addClass("d-none");
$("#selected-equipment-has-no-instruction-manual-hint").removeClass("d-none");
$("a[href='#description-tab']").tab("show");
}
},
function(xhr)
{
console.error(xhr);
}
);
}
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
equipmentTable.search(value).draw();
});
$(document).on('click', '.equipment-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-equipment-name');
var objectId = $(e.currentTarget).attr('data-equipment-id');
bootbox.confirm({
message: L('Are you sure to delete equipment "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Get('delete-object/equipment/' + objectId,
function(result)
{
window.location.href = U('/equipment');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$("#selectedEquipmentInstructionManualToggleFullscreenButton").on('click', function(e)
{
$("#selectedEquipmentInstructionManualCard").toggleClass("fullscreen");
$("#selectedEquipmentInstructionManualCard .card-header").toggleClass("fixed-top");
$("#selectedEquipmentInstructionManualCard .card-body").toggleClass("mt-5");
$("body").toggleClass("fullscreen-responsive-embed-active");
ResizeResponsiveEmbeds(true);
});
$("#selectedEquipmentDescriptionToggleFullscreenButton").on('click', function(e)
{
$("#selectedEquipmentDescriptionCard").toggleClass("fullscreen");
$("#selectedEquipmentDescriptionCard .card-header").toggleClass("fixed-top");
$("#selectedEquipmentDescriptionCard .card-body").toggleClass("mt-5");
});

View File

@@ -0,0 +1,130 @@
$('#save-equipment-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#equipment-form').serializeJSON();
if ($("#instruction-manual")[0].files.length > 0)
{
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
jsonData.instruction_manual_file_name = someRandomStuff + $("#instruction-manual")[0].files[0].name;
}
if (Grocy.DeleteInstructionManualOnSave)
{
jsonData.instruction_manual_file_name = null;
}
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/equipment', jsonData,
function(result)
{
if (jsonData.hasOwnProperty("instruction_manual_file_name") && !Grocy.DeleteInstructionManualOnSave)
{
Grocy.Api.UploadFile($("#instruction-manual")[0].files[0], 'equipmentmanuals', jsonData.instruction_manual_file_name,
function(result)
{
window.location.href = U('/equipment');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = U('/equipment');
}
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
if (Grocy.DeleteInstructionManualOnSave)
{
Grocy.Api.DeleteFile(Grocy.InstructionManualFileNameName, 'equipmentmanuals',
function(result)
{
// Nothing to do
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
};
Grocy.Api.Post('edit-object/equipment/' + Grocy.EditObjectId, jsonData,
function(result)
{
if (jsonData.hasOwnProperty("instruction_manual_file_name") && !Grocy.DeleteInstructionManualOnSave)
{
Grocy.Api.UploadFile($("#instruction-manual")[0].files[0], 'equipmentmanuals', jsonData.instruction_manual_file_name,
function(result)
{
window.location.href = U('/equipment');;
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = U('/equipment');;
}
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#equipment-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('equipment-form');
});
$('#equipment-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('equipment-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-equipment-button').click();
}
}
});
Grocy.DeleteInstructionManualOnSave = false;
$('#delete-current-instruction-manual-button').on('click', function (e)
{
Grocy.DeleteInstructionManualOnSave = true;
$("#current-equipment-instruction-manual").addClass("d-none");
$("#delete-current-instruction-manual-on-save-hint").removeClass("d-none");
$("#delete-current-instruction-manual-button").addClass("disabled");
});
$('#description').summernote({
minHeight: '300px',
lang: L('summernote_locale')
});
ResizeResponsiveEmbeds();
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('equipment-form');

View File

@@ -118,9 +118,10 @@ $('#inventory-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('inventory-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -39,9 +39,10 @@ $('#location-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('location-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -1,4 +1,4 @@
$('#save-product-button').on('click', function(e)
$('#save-product-button').on('click', function (e)
{
e.preventDefault();
@@ -9,14 +9,42 @@
redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val());
}
var jsonData = $('#product-form').serializeJSON();
if ($("#product-picture")[0].files.length > 0)
{
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
jsonData.picture_file_name = someRandomStuff + $("#product-picture")[0].files[0].name;
}
if (Grocy.DeleteProductPictureOnSave)
{
jsonData.picture_file_name = null;
}
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/products', $('#product-form').serializeJSON(),
function(result)
Grocy.Api.Post('add-object/products', jsonData,
function (result)
{
window.location.href = redirectDestination;
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.UploadFile($("#product-picture")[0].files[0], 'productpictures', jsonData.picture_file_name,
function (result)
{
window.location.href = redirectDestination;
},
function (xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = redirectDestination;
}
},
function(xhr)
function (xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
@@ -24,10 +52,40 @@
}
else
{
Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
if (Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.DeleteFile(Grocy.ProductPictureFileName, 'productpictures',
function(result)
{
// Nothing to do
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
};
Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, jsonData,
function(result)
{
window.location.href = redirectDestination;
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.UploadFile($("#product-picture")[0].files[0], 'productpictures', jsonData.picture_file_name,
function(result)
{
window.location.href = redirectDestination;
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = redirectDestination;
}
},
function(xhr)
{
@@ -39,7 +97,8 @@
$('#barcode-taginput').tagsManager({
'hiddenTagListName': 'barcode',
'tagsContainer': '#barcode-taginput-container'
'tagsContainer': '#barcode-taginput-container',
'tagClass': 'badge badge-secondary'
});
if (Grocy.EditMode === 'edit')
@@ -78,7 +137,21 @@ if (prefillBarcode !== undefined)
$('.input-group-qu').on('change', function(e)
{
var quIdPurchase = $("#qu_id_purchase").val();
var quIdStock = $("#qu_id_stock").val();
var factor = $('#qu_factor_purchase_to_stock').val();
if (quIdPurchase != quIdStock)
{
$('#qu_factor_purchase_to_stock').attr("min", 2);
$("#qu_factor_purchase_to_stock").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '2'));
}
else
{
$('#qu_factor_purchase_to_stock').attr("min", 1);
$("#qu_factor_purchase_to_stock").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
}
if (factor > 1)
{
$('#qu-conversion-info').text(L('This means 1 #1 purchased will be converted into #2 #3 in stock', $("#qu_id_purchase option:selected").text(), (1 * factor).toString(), $("#qu_id_stock option:selected").text()));
@@ -88,6 +161,8 @@ $('.input-group-qu').on('change', function(e)
{
$('#qu-conversion-info').addClass('d-none');
}
Grocy.FrontendHelpers.ValidateForm('product-form');
});
$('#product-form input').keyup(function(event)
@@ -99,9 +174,10 @@ $('#product-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else
@@ -111,6 +187,15 @@ $('#product-form input').keydown(function(event)
}
});
Grocy.DeleteProductPictureOnSave = false;
$('#delete-current-product-picture-button').on('click', function (e)
{
Grocy.DeleteProductPictureOnSave = true;
$("#current-product-picture").addClass("d-none");
$("#delete-current-product-picture-on-save-hint").removeClass("d-none");
$("#delete-current-product-picture-button").addClass("disabled");
});
$('#name').focus();
$('.input-group-qu').trigger('change');
Grocy.FrontendHelpers.ValidateForm('product-form');

View File

@@ -39,9 +39,10 @@ $('#product-group-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('product-group-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -35,33 +35,54 @@ $(document).on('click', '.product-delete-button', function (e)
var objectName = $(e.currentTarget).attr('data-product-name');
var objectId = $(e.currentTarget).attr('data-product-id');
bootbox.confirm({
message: L('Are you sure to delete product "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
Grocy.Api.Get('stock/get-product-details/' + objectId,
function(productDetails)
{
var stockAmount = productDetails.stock_amount || '0';
if (stockAmount.toString() == "0")
{
bootbox.confirm({
message: L('Are you sure to delete product "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function (result)
{
if (result === true)
{
Grocy.Api.Get('delete-object/products/' + objectId,
function (result)
{
window.location.href = U('/products');
},
function (xhr)
{
console.error(xhr);
}
);
}
}
});
}
else
{
bootbox.alert({
title: L('Delete not possible'),
message: L('This product cannot be deleted because it is in stock, please remove the stock amount first.') + '<br><br>' + L('Stock amount') + ': ' + stockAmount + ' ' + Pluralize(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)
});
}
},
callback: function(result)
function(xhr)
{
if (result === true)
{
Grocy.Api.Get('delete-object/products/' + objectId,
function(result)
{
window.location.href = U('/products');
},
function(xhr)
{
console.error(xhr);
}
);
}
console.error(xhr);
}
});
);
});

View File

@@ -144,9 +144,10 @@ $('#purchase-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('purchase-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -39,9 +39,10 @@ $('#quantityunit-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('quantityunit-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -58,9 +58,10 @@ $('#recipe-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('recipe-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else
@@ -168,3 +169,8 @@ $("#recipe-pos-add-button").on("click", function(e)
}
);
});
$('#description').summernote({
minHeight: '300px',
lang: L('summernote_locale')
});

View File

@@ -4,7 +4,6 @@
var jsonData = $('#recipe-pos-form').serializeJSON({ checkboxUncheckedValue: "0" });
jsonData.recipe_id = Grocy.EditObjectParentId;
console.log(jsonData);
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/recipes_pos', jsonData,
@@ -88,9 +87,10 @@ $('#recipe-pos-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('recipe-pos-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -158,4 +158,6 @@ recipesTables.on('select', function(e, dt, type, indexes)
$("#selectedRecipeToggleFullscreenButton").on('click', function(e)
{
$("#selectedRecipeCard").toggleClass("fullscreen");
$("#selectedRecipeCard .card-header").toggleClass("fixed-top");
$("#selectedRecipeCard .card-body").toggleClass("mt-5");
});

View File

@@ -85,9 +85,10 @@ $('#shoppinglist-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('shoppinglist-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -141,6 +141,45 @@ $(document).on('click', '.product-consume-button', function(e)
);
});
$(document).on("click", ".product-name-cell", function(e)
{
var productHasPicture = BoolVal($(e.currentTarget).attr("data-product-has-picture"));
if (productHasPicture)
{
var pictureUrl = $(e.currentTarget).attr("data-picture-url");
var productName = $(e.currentTarget).attr("data-product-name");
var productId = $(e.currentTarget).attr("data-product-id");
bootbox.dialog({
title: L("Image of product #1", productName),
message: "<img src='" + pictureUrl + "' class='img-fluid img-thumbnail'>",
backdrop: false,
onEscape: true,
closeButton: false,
className: 'centered-dialog',
buttons: {
editproduct: {
label: '<i class="fas fa-edit"></i> ' + L('Edit product'),
className: 'btn-info responsive-button',
callback: function ()
{
window.location.href = U('/product/' + productId + '?returnto=' + encodeURIComponent(window.location.pathname) + '#product-picture');
}
},
close: {
label: L('Close'),
className: 'btn-default responsive-button',
callback: function()
{
bootbox.hideAll();
}
}
}
});
}
});
function RefreshStatistics()
{
Grocy.Api.Get('stock/get-current-stock',

View File

@@ -39,9 +39,10 @@ $('#task-category-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('task-category-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -44,9 +44,10 @@ $('#task-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('task-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -49,9 +49,10 @@ $('#user-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('user-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -61,6 +61,10 @@ $app->group('', function()
$this->get('/taskcategories', '\Grocy\Controllers\TasksController:TaskCategoriesList');
$this->get('/taskcategory/{categoryId}', '\Grocy\Controllers\TasksController:TaskCategoryEditForm');
// Equipment routes
$this->get('/equipment', '\Grocy\Controllers\EquipmentController:Overview');
$this->get('/equipment/{equipmentId}', '\Grocy\Controllers\EquipmentController:EditForm');
// OpenAPI routes
$this->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi');
$this->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList');
@@ -81,9 +85,12 @@ $app->group('/api', function()
// System
$this->get('/system/get-db-changed-time', '\Grocy\Controllers\SystemApiController:GetDbChangedTime');
$this->post('/system/log-missing-localization', '\Grocy\Controllers\SystemApiController:LogMissingLocalization');
// Files
$this->post('/files/upload/{group}', '\Grocy\Controllers\FilesApiController:Upload');
$this->put('/file/{group}', '\Grocy\Controllers\FilesApiController:UploadFile');
$this->get('/file/{group}', '\Grocy\Controllers\FilesApiController:ServeFile');
$this->delete('/file/{group}', '\Grocy\Controllers\FilesApiController:DeleteFile');
// Users
$this->get('/users/get', '\Grocy\Controllers\UsersApiController:GetUsers');
@@ -91,6 +98,10 @@ $app->group('/api', function()
$this->post('/users/edit/{userId}', '\Grocy\Controllers\UsersApiController:EditUser');
$this->get('/users/delete/{userId}', '\Grocy\Controllers\UsersApiController:DeleteUser');
// User
$this->get('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:GetUserSetting');
$this->post('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:SetUserSetting');
// Stock
$this->get('/stock/add-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:AddProduct');
$this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct');

View File

@@ -14,6 +14,7 @@ class DemoDataGeneratorService extends BaseService
if (intval($rowCount) === 0)
{
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
$loremIpsumWithHtmlFormattings = "<h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p><ul><li>At vero eos et accusam et justo duo dolores et ea rebum.</li><li>Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</li></ul><h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur \r\nsadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et \r\ndolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et\r\n justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea \r\ntakimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit \r\namet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>,\r\n sed diam nonumy eirmod tempor invidunt ut labore et dolore magna \r\naliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo \r\ndolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus \r\nest Lorem ipsum dolor sit amet.</p>";
$sql = "
UPDATE users SET username = '{$localizationService->Localize('Demo User')}' WHERE id = 1;
@@ -39,9 +40,9 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO product_groups(name) VALUES ('06 {$localizationService->Localize('Refrigerated products')}'); --6
DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here...
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8, 1); --1
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8, 1, 'cookies.jpg'); --1
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Chocolate')}', 3, 3, 3, 1, 8, 1); --2
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8, 1); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8, 1, 'gummybears.jpg'); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Crisps')}', 3, 3, 3, 1, 10, 1); --4
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Eggs')}', 2, 3, 2, 10, 5); --5
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Noodles')}', 3, 3, 3, 1, 6); --6
@@ -50,10 +51,10 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Yogurt')}', 2, 6, 6, 1, 6); --9
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cheese')}', 2, 3, 3, 1, 6); --10
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cold cuts')}', 2, 3, 3, 1, 6); --11
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1, 5); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1, 5); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1, 5, 'paprika.jpg'); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1, 5, 'cucumber.jpg'); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Radish')}', 2, 7, 7, 1, 5); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1, 5); --15
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1, 5, 'tomato.jpg'); --15
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Pizza dough')}', 3, 3, 3, 1, 6); --16
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Sieved tomatoes')}', 4, 5, 5, 1, 3); --17
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Salami')}', 2, 3, 3, 1, 6); --18
@@ -66,10 +67,10 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO shopping_list (product_id, amount) VALUES (20, 1);
INSERT INTO shopping_list (product_id, amount) VALUES (17, 1);
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsum}'); --1
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsum}'); --2
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsum}'); --3
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsum}'); --4
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsumWithHtmlFormattings}'); --1
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsumWithHtmlFormattings}'); --2
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsumWithHtmlFormattings}'); --3
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsumWithHtmlFormattings}'); --4
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 16, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 17, 1);
@@ -105,6 +106,9 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Find a solution for what to do when I forget the door keys')}', date(datetime('now', 'localtime'), '+3 day'), 1);
INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}3', date(datetime('now', 'localtime'), '+4 day'), 1);
INSERT INTO equipment (name, description, instruction_manual_file_name) VALUES ('{$localizationService->Localize('Coffee machine')}', '{$loremIpsumWithHtmlFormattings}', 'loremipsum.pdf'); --1
INSERT INTO equipment (name, description) VALUES ('{$localizationService->Localize('Dishwasher')}', '{$loremIpsumWithHtmlFormattings}'); --2
INSERT INTO migrations (migration) VALUES (-1);
";
@@ -201,6 +205,25 @@ class DemoDataGeneratorService extends BaseService
$batteriesService->TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-50 days')));
$batteriesService->TrackChargeCycle(3, date('Y-m-d H:i:s', strtotime('-65 days')));
$batteriesService->TrackChargeCycle(4, date('Y-m-d H:i:s', strtotime('-56 days')));
// Download demo storage data
$productPicturesFolder = GROCY_DATAPATH . '/storage/productpictures';
$equipmentManualsFolder = GROCY_DATAPATH . '/storage/equipmentmanuals';
mkdir(GROCY_DATAPATH . '/storage');
mkdir(GROCY_DATAPATH . '/storage/productpictures');
mkdir(GROCY_DATAPATH . '/storage/equipmentmanuals');
$sslOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
);
file_put_contents("$productPicturesFolder/cookies.jpg", file_get_contents('https://releases.grocy.info/demoresources/cookies.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/cucumber.jpg", file_get_contents('https://releases.grocy.info/demoresources/cucumber.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/gummybears.jpg", file_get_contents('https://releases.grocy.info/demoresources/gummybears.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/paprika.jpg", file_get_contents('https://releases.grocy.info/demoresources/paprika.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/tomato.jpg", file_get_contents('https://releases.grocy.info/demoresources/tomato.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$equipmentManualsFolder/loremipsum.pdf", file_get_contents('https://releases.grocy.info/demoresources/loremipsum.pdf', false, stream_context_create($sslOptions)));
}
}

View File

@@ -34,7 +34,7 @@ class LocalizationService
}
}
private function LogMissingLocalization(string $culture, string $text)
public function LogMissingLocalization(string $culture, string $text)
{
$file = GROCY_DATAPATH . "/missing_translations_$culture.json";

View File

@@ -8,9 +8,14 @@ class StockService extends BaseService
const TRANSACTION_TYPE_CONSUME = 'consume';
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
public function GetCurrentStock()
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
{
$sql = 'SELECT * from stock_current';
if ($includeNotInStockButMissingProducts)
{
$sql = 'SELECT * from stock_current WHERE best_before_date IS NOT NULL';
}
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
@@ -20,10 +25,17 @@ class StockService extends BaseService
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetExpiringProducts(int $days = 5)
public function GetExpiringProducts(int $days = 5, bool $excludeExpired = false)
{
$currentStock = $this->GetCurrentStock();
return FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime("+$days days")), '<');
$currentStock = $this->GetCurrentStock(true);
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime("+$days days")), '<');
if ($excludeExpired)
{
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('now')), '>');
}
return $currentStock;
}
public function GetProductDetails(int $productId)

View File

@@ -50,6 +50,55 @@ class UsersService extends BaseService
return $returnUsers;
}
public function GetUserSetting($userId, $settingKey)
{
$settingRow = $this->Database->user_settings()->where('user_id = :1 AND key = :2', $userId, $settingKey)->fetch();
if ($settingRow !== null)
{
return $settingRow->value;
}
else
{
return null;
}
}
public function GetUserSettings($userId)
{
$settings = array();
$settingRows = $this->Database->user_settings()->where('user_id = :1', $userId)->fetchAll();
foreach ($settingRows as $settingRow)
{
$settings[$settingRow->key] = $settingRow->value;
}
// Use the configured default values for all missing settings
global $GROCY_DEFAULT_USER_SETTINGS;
return array_merge($GROCY_DEFAULT_USER_SETTINGS, $settings);
}
public function SetUserSetting($userId, $settingKey, $settingValue)
{
$settingRow = $this->Database->user_settings()->where('user_id = :1 AND key = :2', $userId, $settingKey)->fetch();
if ($settingRow !== null)
{
$settingRow->update(array(
'value' => $settingValue,
'row_updated_timestamp' => date('Y-m-d H:i:s')
));
}
else
{
$settingRow = $this->Database->user_settings()->createRow(array(
'user_id' => $userId,
'key' => $settingKey,
'value' => $settingValue
));
$settingRow->save();
}
}
private function UserExists($userId)
{
$userRow = $this->Database->users()->where('id = :1', $userId)->fetch();

View File

@@ -1,4 +1,4 @@
{
"Version": "1.19.1",
"ReleaseDate": "2018-09-27"
"Version": "1.21.0",
"ReleaseDate": "2018-10-06"
}

View File

@@ -47,7 +47,7 @@
'invalidFeedback' => $L('This cannot be negative')
))
<button id="save-battery-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-battery-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -32,7 +32,7 @@
'invalidFeedback' => $L('This can only be before now')
))
<button id="save-batterytracking-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
<button id="save-batterytracking-button" class="btn btn-success">{{ $L('OK') }}</button>
</form>
</div>

View File

@@ -53,7 +53,7 @@
'additionalHtmlElements' => '<p id="chore-period-type-info" class="form-text text-muted small d-none"></p>'
))
<button id="save-chore-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-chore-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -39,7 +39,7 @@
'prefillByUserId' => GROCY_USER_ID
))
<button id="save-choretracking-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
<button id="save-choretracking-button" class="btn btn-success">{{ $L('OK') }}</button>
</form>
</div>

View File

@@ -15,6 +15,10 @@
<strong>{{ $L('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago" class="timeago timeago-contextual"></time><br>
<strong>{{ $L('Last price') }}:</strong> <span id="productcard-product-last-price"></span>
<h5 class="mt-3">{{ $L('Product picture') }}</h5>
<img id="productcard-product-picture" src="" class="img-fluid img-thumbnail d-none">
<span id="productcard-no-product-picture" class="font-italic d-none">{{ $L('No picture') }}</span>
<h5 class="mt-3">{{ $L('Price history') }}</h5>
<canvas id="productcard-product-price-history-chart" class="w-100 d-none"></canvas>
<span id="productcard-no-price-data-hint" class="font-italic d-none">{{ $L('No price history available') }}</span>

View File

@@ -32,7 +32,7 @@
</label>
</div>
<button id="save-consume-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
<button id="save-consume-button" class="btn btn-success">{{ $L('OK') }}</button>
</form>
</div>

91
views/equipment.blade.php Normal file
View File

@@ -0,0 +1,91 @@
@extends('layout.default')
@section('title', $L('Equipment'))
@section('activeNav', 'equipment')
@section('viewJsName', 'equipment')
@section('content')
<div class="row">
<div class="col-xs-12 col-md-4 pb-3">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/equipment/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
<table id="equipment-table" class="table table-striped dt-responsive">
<thead>
<tr>
<th>{{ $L('Name') }}</th>
</tr>
</thead>
<tbody>
@foreach($equipment as $equipmentItem)
<tr data-equipment-id="{{ $equipmentItem->id }}">
<td>
{{ $equipmentItem->name }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="col-xs-12 col-md-8">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#instruction-manual-tab">{{ $L('Instruction manual') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#description-tab">{{ $L('Notes') }}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="instruction-manual-tab">
<div id="selectedEquipmentInstructionManualCard" class="card">
<div class="card-header">
<i class="fas fa-toolbox"></i> <span class="selected-equipment-name"></span>&nbsp;&nbsp;
<a class="btn btn-info btn-sm btn-outline-info py-0" href="{{ $U('/equipment/') }}{{ $equipmentItem->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm btn-outline-danger py-0 equipment-delete-button" href="#" data-equipment-id="{{ $equipmentItem->id }}" data-equipment-name="{{ $equipmentItem->name }}">
<i class="fas fa-trash"></i>
</a>
<a id="selectedEquipmentInstructionManualToggleFullscreenButton" class="btn btn-sm btn-outline-secondary py-0 float-right" href="#" data-toggle="tooltip" title="{{ $L('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i>
</a>
</div>
<div class="card-body py-0 px-0">
<p id="selected-equipment-has-no-instruction-manual-hint" class="text-muted font-italic d-none">{{ $L('The selected equipment has no instruction manual') }}</p>
<embed id="selected-equipment-instruction-manual" class="embed-responsive embed-responsive-4by3" src="" type="application/pdf">
</div>
</div>
</div>
<div class="tab-pane fade" id="description-tab">
<div id="selectedEquipmentDescriptionCard" class="card">
<div class="card-header">
<i class="fas fa-toolbox"></i> <span class="selected-equipment-name"></span>&nbsp;&nbsp;
<a class="btn btn-info btn-sm btn-outline-info py-0" href="{{ $U('/equipment/') }}{{ $equipmentItem->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm btn-outline-danger py-0 equipment-delete-button" href="#" data-equipment-id="{{ $equipmentItem->id }}" data-equipment-name="{{ $equipmentItem->name }}">
<i class="fas fa-trash"></i>
</a>
<a id="selectedEquipmentDescriptionToggleFullscreenButton" class="btn btn-sm btn-outline-secondary py-0 float-right" href="#" data-toggle="tooltip" title="{{ $L('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i>
</a>
</div>
<div class="card-body">
<div id="description-tab-content" class="mb-0"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@stop

View File

@@ -0,0 +1,74 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $L('Edit equipment'))
@else
@section('title', $L('Create equipment'))
@endif
@section('viewJsName', 'equipmentform')
@push('pageScripts')
<script src="{{ $U('/node_modules/summernote/dist/summernote-bs4.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('summernote_locale')))<script src="{{ $U('/node_modules', true) }}/summernote/dist/lang/summernote-{{ $L('summernote_locale') }}.js?v={{ $version }}"></script>@endif
@endpush
@push('pageStyles')
<link href="{{ $U('/node_modules/summernote/dist/summernote-bs4.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $equipment->id }};</script>
@if(!empty($equipment->instruction_manual_file_name))
<script>Grocy.InstructionManualFileNameName = '{{ $equipment->instruction_manual_file_name }}';</script>
@endif
@endif
<form id="equipment-form" novalidate>
<div class="form-group">
<label for="name">{{ $L('Name') }}</label>
<input type="text" class="form-control" required id="name" name="name" value="@if($mode == 'edit'){{ $equipment->name }}@endif">
<div class="invalid-feedback">{{ $L('A name is required') }}</div>
</div>
<div class="form-group">
<label for="instruction-manual">{{ $L('Instruction manual') }} (PDF)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="instruction-manual" accept="application/pdf">
<label class="custom-file-label" for="instruction-manual">{{ $L('No file selected') }}</label>
</div>
<p class="form-text text-muted small">{{ $L('If you don\'t select a file, the current instruction manual will not be altered') }}</p>
</div>
<div class="form-group">
<label for="description">{{ $L('Notes') }}</label>
<textarea class="form-control" id="description" name="description">@if($mode == 'edit'){{ $equipment->description }}@endif</textarea>
</div>
<button id="save-equipment-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>
<div class="col-lg-6 col-xs-12">
<label class="mt-2">{{ $L('Current instruction manual') }}</label>
<button id="delete-current-instruction-manual-button" class="btn btn-sm btn-danger @if(empty($equipment->instruction_manual_file_name)) disabled @endif"><i class="fas fa-trash"></i> {{ $L('Delete') }}</button>
@if(!empty($equipment->instruction_manual_file_name))
<embed id="current-equipment-instruction-manual" class="embed-responsive embed-responsive-4by3" src="{{ $U('/api/file/equipmentmanuals?file_name=' . $equipment->instruction_manual_file_name) }}" type="application/pdf">
<p id="delete-current-instruction-manual-on-save-hint" class="form-text text-muted font-italic d-none">{{ $L('The current instruction manual will be deleted when you save the equipment') }}</p>
@else
<p id="no-current-instruction-manual-hint" class="form-text text-muted font-italic">{{ $L('No instruction manual available') }}</p>
@endif
</div>
</div>
@stop

View File

@@ -42,7 +42,7 @@
'shortcutLabel' => 'Never expires'
))
<button id="save-inventory-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
<button id="save-inventory-button" class="btn btn-success">{{ $L('OK') }}</button>
</form>
</div>

View File

@@ -26,6 +26,7 @@
<link href="{{ $U('/node_modules/tempusdominus-bootstrap-4/build/css/tempusdominus-bootstrap-4.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/components_unmanaged/noto-sans-v6-latin/noto-sans-v6-latin.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/css/grocy.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/css/grocy_night_mode.css?v=', true) }}{{ $version }}" rel="stylesheet">
@stack('pageStyles')
@if(file_exists(GROCY_DATAPATH . '/custom_css.html'))
@@ -35,15 +36,17 @@
<script>
var Grocy = { };
Grocy.Components = { };
Grocy.Mode = '{{ GROCY_MODE }}';
Grocy.BaseUrl = '{{ $U('/') }}';
Grocy.LocalizationStrings = {!! json_encode($localizationStrings) !!};
Grocy.ActiveNav = '@yield('activeNav', '')';
Grocy.Culture = '{{ GROCY_CULTURE }}';
Grocy.Currency = '{{ GROCY_CURRENCY }}';
Grocy.UserSettings = {!! json_encode($userSettings) !!};
</script>
</head>
<body class="fixed-nav">
<body class="fixed-nav @if(boolval($userSettings['night_mode_enabled']) || (boolval($userSettings['auto_night_mode_enabled']) && boolval($userSettings['currently_inside_night_mode_range']))) night-mode @endif">
<nav id="mainNav" class="navbar navbar-expand-lg navbar-light fixed-top">
<a class="navbar-brand py-0" href="{{ $U('/') }}"><img src="{{ $U('/img/grocy_logo.svg?v=', true) }}{{ $version }}" height="30"></a>
@@ -90,6 +93,12 @@
<span class="nav-link-text">{{ $L('Batteries overview') }}</span>
</a>
</li>
<li class="nav-item" data-toggle="tooltip" data-placement="right" title="{{ $L('Equipment') }}" data-nav-for-page="equipment">
<a class="nav-link discrete-link" href="{{ $U('/equipment') }}">
<i class="fas fa-toolbox"></i>
<span class="nav-link-text">{{ $L('Equipment') }}</span>
</a>
</li>
<li class="nav-item mt-4" data-toggle="tooltip" data-placement="right" title="{{ $L('Purchase') }}" data-nav-for-page="purchase">
<a class="nav-link discrete-link" href="{{ $U('/purchase') }}">
@@ -195,6 +204,51 @@
</li>
@endif
@if(GROCY_AUTHENTICATED === true)
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-sliders-h"></i> <span class="d-inline d-lg-none">{{ $L('View settings') }}</span></a>
<div class="dropdown-menu dropdown-menu-right">
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input user-setting-control" type="checkbox" id="auto-reload-enabled" data-setting-key="auto_reload_on_db_change">
<label class="form-check-label" for="auto-reload-enabled">
{{ $L('Auto reload on external changes') }}
</label>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input user-setting-control" type="checkbox" id="night-mode-enabled" data-setting-key="night_mode_enabled">
<label class="form-check-label" for="night-mode-enabled">
{{ $L('Enable night mode') }}
</label>
</div>
</div>
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input user-setting-control" type="checkbox" id="auto-night-mode-enabled" data-setting-key="auto_night_mode_enabled">
<label class="form-check-label" for="auto-night-mode-enabled">
{{ $L('Auto enable in time range') }}
</label>
</div>
<div class="form-inline">
<input type="text" class="form-control my-1 user-setting-control" readonly id="auto-night-mode-time-range-from" placeholder="{{ $L('From') }} ({{ $L('in format') }} HH:mm)" data-setting-key="auto_night_mode_time_range_from">
<input type="text" class="form-control user-setting-control" readonly id="auto-night-mode-time-range-to" placeholder="{{ $L('To') }} ({{ $L('in format') }} HH:mm)" data-setting-key="auto_night_mode_time_range_to">
</div>
<div class="form-check mt-1">
<input class="form-check-input user-setting-control" type="checkbox" id="auto-night-mode-time-range-goes-over-midgnight" data-setting-key="auto_night_mode_time_range_goes_over_midnight">
<label class="form-check-label" for="auto-night-mode-time-range-goes-over-midgnight">
{{ $L('Time range goes over midnight') }}
</label>
</div>
<input class="form-check-input d-none user-setting-control" type="checkbox" id="currently-inside-night-mode-range" data-setting-key="currently_inside_night_mode_range">
</div>
</div>
</li>
@endif
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="d-inline d-lg-none">{{ $L('Settings') }}</span></a>
@@ -270,6 +324,7 @@
<script src="{{ $U('/js/extensions.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy_dbchangedhandling.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy_nightmode.js?v=', true) }}{{ $version }}"></script>
@stack('pageScripts')
@stack('componentScripts')
<script src="{{ $U('/viewjs', true) }}/@yield('viewJsName').js?v={{ $version }}"></script>

View File

@@ -32,7 +32,7 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $location->description }}@endif</textarea>
</div>
<button id="save-location-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-location-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -29,7 +29,7 @@
</label>
</div>
<button id="login-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
<button id="login-button" class="btn btn-success">{{ $L('OK') }}</button>
</form>
</div>

View File

@@ -10,6 +10,7 @@
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
@@ -17,6 +18,10 @@
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $product->id }};</script>
@if(!empty($product->picture_file_name))
<script>Grocy.ProductPictureFileName = '{{ $product->picture_file_name }}';</script>
@endif
@endif
<form id="product-form" novalidate>
@@ -108,8 +113,28 @@
'additionalHtmlElements' => '<p id="qu-conversion-info" class="form-text text-muted small d-none"></p>'
))
<button id="save-product-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<div class="form-group">
<label for="product-picture">{{ $L('Product picture') }}</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="product-picture" accept="image/*">
<label class="custom-file-label" for="product-picture">{{ $L('No file selected') }}</label>
</div>
<p class="form-text text-muted small">{{ $L('If you don\'t select a file, the current picture will not be altered') }}</p>
</div>
<button id="save-product-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>
<div class="col-lg-6 col-xs-12">
<label class="mt-2">{{ $L('Current picture') }}</label>
<button id="delete-current-product-picture-button" class="btn btn-sm btn-danger @if(empty($product->picture_file_name)) disabled @endif"><i class="fas fa-trash"></i> {{ $L('Delete') }}</button>
@if(!empty($product->picture_file_name))
<p><img id="current-product-picture" src="{{ $U('/api/file/productpictures?file_name=' . $product->picture_file_name) }}" class="img-fluid img-thumbnail mt-2"></p>
<p id="delete-current-product-picture-on-save-hint" class="form-text text-muted font-italic d-none">{{ $L('The current picture will be deleted when you save the product') }}</p>
@else
<p id="no-current-product-picture-hint" class="form-text text-muted font-italic">{{ $L('No picture') }}</p>
@endif
</div>
</div>
@stop

View File

@@ -32,7 +32,7 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $group->description }}@endif</textarea>
</div>
<button id="save-product-group-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-product-group-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -49,7 +49,7 @@
'isRequired' => false
))
<button id="save-purchase-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
<button id="save-purchase-button" class="btn btn-success">{{ $L('OK') }}</button>
</form>
</div>

View File

@@ -37,7 +37,7 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $quantityunit->description }}@endif</textarea>
</div>
<button id="save-quantityunit-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-quantityunit-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -8,6 +8,15 @@
@section('viewJsName', 'recipeform')
@push('pageScripts')
<script src="{{ $U('/node_modules/summernote/dist/summernote-bs4.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('summernote_locale')))<script src="{{ $U('/node_modules', true) }}/summernote/dist/lang/summernote-{{ $L('summernote_locale') }}.js?v={{ $version }}"></script>@endif
@endpush
@push('pageStyles')
<link href="{{ $U('/node_modules/summernote/dist/summernote-bs4.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@section('content')
<div class="row">
<div class="col">
@@ -33,10 +42,10 @@
<div class="form-group">
<label for="description">{{ $L('Preparation') }}</label>
<textarea id="description" class="form-control" name="description" rows="25">@if($mode == 'edit'){{ $recipe->description }}@endif</textarea>
<textarea id="description" class="form-control" name="description">@if($mode == 'edit'){{ $recipe->description }}@endif</textarea>
</div>
<button id="save-recipe-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-recipe-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -73,7 +73,7 @@
<textarea class="form-control" rows="2" id="note" name="note">@if($mode == 'edit'){{ $recipePos->note }}@endif</textarea>
</div>
<button id="save-recipe-pos-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-recipe-pos-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -6,18 +6,15 @@
@section('content')
<div class="row">
<div class="col">
<div class="col-xs-12 col-md-6 pb-3">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/recipe/new') }}">
<i class="fas fa-plus"></i> {{ $L('Add') }}
</a>
</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-6 pb-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
@@ -82,7 +79,7 @@
</ul>
<div class="card-body">
<h5>{{ $L('Preparation') }}</h5>
{!! nl2br(htmlentities($selectedRecipe->description)) !!}
{!! $selectedRecipe->description !!}
</div>
</div>
</div>

View File

@@ -42,7 +42,7 @@
<textarea class="form-control" rows="2" id="note" name="note">@if($mode == 'edit'){{ $listItem->note }}@endif</textarea>
</div>
<button id="save-shoppinglist-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-shoppinglist-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -8,6 +8,14 @@
<script src="{{ $U('/node_modules/jquery-ui-dist/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@push('pageStyles')
<style>
.product-name-cell[data-product-has-picture='true'] {
cursor: pointer;
}
</style>
@endpush
@section('content')
<div class="row">
<div class="col">
@@ -58,16 +66,16 @@
</thead>
<tbody>
@foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days"))) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<td class="fit-content">
<a class="btn btn-success btn-sm product-consume-button" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Consume #3 #1 of #2', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Consume #3 #1 of #2', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-consume-amount="1">
<i class="fas fa-utensils"></i> 1
</a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="btn btn-danger btn-sm product-consume-button" href="#" data-toggle="tooltip" data-placement="right" title="{{ $L('Consume all #1 which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $L('Consume all #1 which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
@@ -75,8 +83,12 @@
<i class="fas fa-utensils"></i> {{ $L('All') }}
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
<td class="product-name-cell"
data-picture-url="{{ $U('/api/file/productpictures?file_name=' . FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-has-picture="{{ BoolToString(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount">{{ $currentStockEntry->amount }}</span> {{ Pluralize($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
@@ -89,7 +101,7 @@
{{ FindObjectInArrayByPropertyValue($locations, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->location_id)->name }}
</td>
<td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days"))) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
</td>
</tr>
@endforeach

View File

@@ -32,7 +32,7 @@
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $category->description }}@endif</textarea>
</div>
<button id="save-task-category-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-task-category-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -76,7 +76,7 @@
'prefillByUserId' => $initUserId
))
<button id="save-task-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-task-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -48,7 +48,7 @@
<div class="invalid-feedback">{{ $L('Passwords do not match') }}</div>
</div>
<button id="save-user-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-user-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

@@ -4,13 +4,13 @@
"@danielfarrell/bootstrap-combobox@https://github.com/berrnd/bootstrap-combobox.git#master":
version "1.1.8"
resolved "https://github.com/berrnd/bootstrap-combobox.git#d5a43b011d4d2c86537df26e15d2caa51be6a15f"
resolved "https://github.com/berrnd/bootstrap-combobox.git#fcf0110146f4daab94888234c57d198b4ca5f129"
"@fortawesome/fontawesome-free@^5.1.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.3.1.tgz#5466b8f31c1f493a96754c1426c25796d0633dd9"
"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2", "tagmanager@https://github.com/max-favilli/tagmanager.git#3.0.2":
"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2":
version "3.0.1"
resolved "https://github.com/max-favilli/tagmanager.git#df9eb9935c8585a392dfc00602f890caf233fa94"
dependencies:
@@ -108,16 +108,16 @@ datatables.net-responsive@2.2.3, datatables.net-responsive@^2.2.3:
jquery ">=1.7"
datatables.net-rowgroup-bs4@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.0.4.tgz#602f056f9a60bab1b3ac3a36088636f40156b05a"
version "1.1.0"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.1.0.tgz#bcaa9842bc9cf70eeba19e8af6edad190c7b896e"
dependencies:
datatables.net-bs4 "^1.10.15"
datatables.net-rowgroup "1.0.4"
datatables.net-rowgroup "1.1.0"
jquery ">=1.7"
datatables.net-rowgroup@1.0.4, datatables.net-rowgroup@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.0.4.tgz#2caf979f28747be7d9ab66725b639b73099d8eb0"
datatables.net-rowgroup@1.1.0, datatables.net-rowgroup@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.1.0.tgz#638efb37a1a15f5b3402b7dbce89b3bcdc286f1a"
dependencies:
datatables.net "^1.10.15"
jquery ">=1.7"
@@ -202,9 +202,13 @@ startbootstrap-sb-admin@^4.0.0:
jquery "3.3.1"
jquery.easing "^1.4.1"
summernote@^0.8.10:
version "0.8.10"
resolved "https://registry.yarnpkg.com/summernote/-/summernote-0.8.10.tgz#21a5d7f18a3b07500b58b60d5907417a54897520"
swagger-ui-dist@^3.17.3:
version "3.19.0"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.0.tgz#95942ce1a556e7fe2705d7c92c6004a628d53207"
version "3.19.2"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.2.tgz#3218f205e7cbc9f0c7c11fabbee07340173ae939"
tempusdominus-bootstrap-4@^5.0.1:
version "5.1.1"