mirror of
https://github.com/grocy/grocy.git
synced 2025-09-16 09:51:30 +00:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6202e8bda7 | ||
|
9984e8f218 | ||
|
b0c91f6ad1 | ||
|
9e24586190 | ||
|
7ba6fc875b | ||
|
3b10906e78 | ||
|
ebd24bf30e | ||
|
ebd9b1b851 | ||
|
b242a5de52 | ||
|
81ec011095 | ||
|
2a371cc081 | ||
|
edb986ce24 | ||
|
f90faca62e | ||
|
6090ac621e | ||
|
ae58606d04 | ||
|
bb9caf9cc9 | ||
|
9dd57decdf | ||
|
f1fc0ee549 | ||
|
fcdeb33426 | ||
|
44cd26ae77 | ||
|
04f34ea6b0 | ||
|
e5fb609c8e | ||
|
c675b534ef | ||
|
6c74881f95 | ||
|
756ec319cc | ||
|
ba2d32be60 | ||
|
7cc09cec67 | ||
|
8b815fce93 | ||
|
f1c78659be | ||
|
5c79a80f7a | ||
|
f451e65278 | ||
|
176333df5b | ||
|
d4227d2e41 | ||
|
0bbd2d9880 | ||
|
b81316bd60 | ||
|
d11dcb38fe | ||
|
77d82f22dc | ||
|
be326a5211 | ||
|
83624eaf27 | ||
|
055619d275 | ||
|
cda3dde120 | ||
|
5a0b862d22 | ||
|
bb5fd8360b | ||
|
d7180bd7b2 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.vscode
|
||||
.gitignore
|
||||
build.bat
|
||||
Dockerfile
|
||||
.DS_store
|
58
Dockerfile-grocy
Normal file
58
Dockerfile-grocy
Normal 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
32
Dockerfile-grocy-nginx
Normal 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;"]
|
12
README.md
12
README.md
@@ -6,7 +6,7 @@ ERP beyond your fridge
|
||||
- Public demo of the latest pre-release version (current master branch) → [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
74
composer.lock
generated
@@ -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",
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
31
controllers/EquipmentController.php
Normal file
31
controllers/EquipmentController.php
Normal 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
{
|
||||
|
@@ -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(
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
30
docker-compose.yml
Normal 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
28
docker_nginx/common.conf
Normal 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;
|
||||
}
|
8
docker_nginx/conf.d/default.conf
Normal file
8
docker_nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,8 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
root /www/public; # see: volumes_from
|
||||
|
||||
include /etc/nginx/common.conf;
|
||||
}
|
20
docker_nginx/conf.d/ssl.conf
Normal file
20
docker_nginx/conf.d/ssl.conf
Normal 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
42
docker_nginx/nginx.conf
Normal 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;
|
||||
}
|
@@ -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": {
|
||||
|
@@ -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
6
info.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
// Show all information, defaults to INFO_ALL
|
||||
phpinfo();
|
||||
|
||||
?>
|
@@ -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'
|
||||
);
|
||||
|
@@ -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' => ''
|
||||
);
|
||||
|
@@ -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',
|
||||
|
@@ -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'
|
||||
);
|
||||
|
@@ -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
27
migrations/0038.sql
Normal 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
10
migrations/0039.sql
Normal 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
2
migrations/0040.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE products
|
||||
ADD picture_file_name TEXT;
|
7
migrations/0041.sql
Normal file
7
migrations/0041.sql
Normal 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'))
|
||||
)
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
221
public/css/grocy_night_mode.css
Normal file
221
public/css/grocy_night_mode.css
Normal 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;
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
@@ -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"));
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
|
106
public/js/grocy_nightmode.js
Normal file
106
public/js/grocy_nightmode.js
Normal 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);
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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'));
|
||||
},
|
||||
|
@@ -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
128
public/viewjs/equipment.js
Normal 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");
|
||||
});
|
130
public/viewjs/equipmentform.js
Normal file
130
public/viewjs/equipmentform.js
Normal 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');
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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');
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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')
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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");
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
13
routes.php
13
routes.php
@@ -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');
|
||||
|
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"Version": "1.19.1",
|
||||
"ReleaseDate": "2018-09-27"
|
||||
"Version": "1.21.0",
|
||||
"ReleaseDate": "2018-10-06"
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
91
views/equipment.blade.php
Normal 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> {{ $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>
|
||||
<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>
|
||||
<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
|
74
views/equipmentform.blade.php
Normal file
74
views/equipmentform.blade.php
Normal 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
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
24
yarn.lock
24
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user