Compare commits

...

67 Commits

Author SHA1 Message Date
Bernd Bestel
756ec319cc Update dependencies for next release 2018-09-30 20:11:24 +02:00
Bernd Bestel
ba2d32be60 Fixes for auto night mode (references #71) 2018-09-30 19:31:03 +02:00
Bernd Bestel
7cc09cec67 The last fix (maybe) for auto night mode handling (references #71) 2018-09-30 18:07:28 +02:00
Bernd Bestel
8b815fce93 Finalize auto night mode feature (references #71) 2018-09-30 18:02:59 +02:00
Bernd Bestel
f1c78659be Optimize user settings 2018-09-30 17:14:04 +02:00
Bernd Bestel
5c79a80f7a Prepared auto night mode configuration option (references #71) 2018-09-30 13:33:21 +02:00
Bernd Bestel
f451e65278 Also log missing localization found in frontend (only when MODE == dev) 2018-09-30 13:02:07 +02:00
Bernd Bestel
176333df5b Save night mode enabled state and apply night mode class to <body> on server side (references #71) 2018-09-30 11:25:07 +02:00
Bernd Bestel
d4227d2e41 Make auto reloading the page on external database changes configurable (closes #74) 2018-09-30 11:17:28 +02:00
Bernd Bestel
0bbd2d9880 Prepare user settings API (references #74 and #71) 2018-09-30 10:47:56 +02:00
Bernd Bestel
b81316bd60 Include products which are not in stock currently but below min. stock amount on stock overview page (fixes #69) 2018-09-30 10:13:37 +02:00
Bernd Bestel
d11dcb38fe Only reload the page on external changes when there is no unsaved form data (fixes #73) 2018-09-30 09:57:42 +02:00
Bernd Bestel
77d82f22dc Fixed scrolling did not work when showing a recipe in fullscreen mode (fixes #76) 2018-09-30 09:41:22 +02:00
Talmai Oliveira
be326a5211 Grocy docker patch (#78)
* typo corrections

* more typos

* initial work towards dockerized version of grocy

* placeholder for future README

* fully working dockerized grocy

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

* Update no.php
2018-09-27 20:39:11 +02:00
Bernd Bestel
8c9b0dedb2 Release v1.19.1 2018-09-27 14:10:28 +02:00
Bernd Bestel
9c2c2c1fa2 Prepare file upload API (references #58) 2018-09-27 14:01:00 +02:00
Bernd Bestel
596dc9e36d Don't show tooltips on touch input devices (this closes #67) 2018-09-27 13:33:03 +02:00
Bernd Bestel
b2019ba42d Fixed new-chore-form did not work (fixes #68) 2018-09-27 12:54:06 +02:00
Bernd Bestel
003d4a567a Use the last commit time as release date when MODE is prerelease 2018-09-25 16:33:09 +02:00
Bernd Bestel
5112e0f551 Next attempt to fix tooltip flickering problems (references #66 and #51) 2018-09-25 16:24:43 +02:00
Bernd Bestel
8008fcdc65 Next attempt to fix #56 2018-09-25 15:52:38 +02:00
Bernd Bestel
8d41dcc650 Use current commit hash as version "number" when MODE is prerelease 2018-09-25 08:55:25 +02:00
Bernd Bestel
037d024862 Also don't remember column searches for all data tables (this now closes #60) 2018-09-25 08:50:28 +02:00
Bernd Bestel
03ca5cd45b Only detect a change when the new database changed time is actually AFTER the last remembered one (references #59) 2018-09-25 08:44:12 +02:00
Bernd Bestel
60d47bef84 Fixed line break 2018-09-24 19:17:42 +02:00
Bernd Bestel
98a7bcb044 Added info about pre-release demo 2018-09-24 19:16:19 +02:00
Bernd Bestel
7401971884 Make info bars clickable and add a filter for them on all overview pages (references #60) 2018-09-24 19:13:53 +02:00
Bernd Bestel
067a10e1b2 Hotfix (will be included in v1.19.0 release): Fixed a regression bug regarding issue #56 (product-flow-popup did not work anymore) 2018-09-24 16:50:30 +02:00
Bernd Bestel
ddfe33fab6 Update dependencies for next release 2018-09-24 13:57:20 +02:00
Bernd Bestel
2a0ec30bb0 Auto reload the current page when the database has changed and when idling (closes #59) 2018-09-24 13:53:18 +02:00
Bernd Bestel
8540fc44f3 Added option to stay logged in permanently 2018-09-24 13:16:57 +02:00
Bernd Bestel
66095738e3 Added product groups (this closes #55) 2018-09-24 13:02:52 +02:00
Bernd Bestel
e472711d23 Fixed strange (and still kind of unknown) problem in productpicker (fixes #56) 2018-09-24 09:51:55 +02:00
Bernd Bestel
8e054a4981 Fix scrolling to top of page when dynamically removing a table row (fixes #57) 2018-09-24 09:30:26 +02:00
Bernd Bestel
feb28211d8 Slightly reordered the main menu 2018-09-24 09:16:53 +02:00
Bernd Bestel
06f25b7006 Finish first version of tasks feature 2018-09-23 19:26:13 +02:00
Bernd Bestel
f85a67a1ff Continue working on tasks feature 2018-09-23 09:22:54 +02:00
Bernd Bestel
6fe0100927 Start working on tasks feature 2018-09-22 22:01:32 +02:00
Bernd Bestel
bcb359e317 Fixed custom JS/CSS was not included on API doc page 2018-09-22 13:28:49 +02:00
Bernd Bestel
4075067a10 Renamed habits to chores as this is more what it is about 2018-09-22 13:26:58 +02:00
Bernd Bestel
bd3c63218b Fixed missing translation in productpicker 2018-09-22 10:58:17 +02:00
Bernd Bestel
27daf384da Respect X-Forwarded-Proto header in UrlManager (closes #54) 2018-09-21 12:49:01 +02:00
Bernd Bestel
905fc0f357 Hotfix (will be include in v1.18.1 release): Price input on purchase page was not optional 2018-09-08 14:31:42 +02:00
Bernd Bestel
9cd0e4ab2d Update dependencies for next release 2018-09-08 14:14:23 +02:00
Bernd Bestel
6b38cd450f Finalized latest changes 2018-09-08 14:06:19 +02:00
Bernd Bestel
bb60f5f043 Typo... 2018-09-08 12:05:44 +02:00
Bernd Bestel
e777be4d3b Replaced the default number input arrow buttons with own ones to better support touch input (references #44) 2018-09-08 12:04:31 +02:00
Bernd Bestel
8a71d55f0f Added missing German translation for last changes 2018-09-08 09:27:50 +02:00
Bernd Bestel
b01b49d10c Show generic error message on saving master data (this closes #45) 2018-09-08 09:26:12 +02:00
Bernd Bestel
496594d898 Don't save filters across page reloads for all data tables (fixes #52) 2018-09-08 08:56:32 +02:00
Bernd Bestel
1d5e82c341 Fixed tooltip flickering problems (this closes #51) 2018-09-08 08:49:09 +02:00
Bernd Bestel
a9b696f41c Fixed datetimepicker (this closes #43) 2018-09-08 08:36:45 +02:00
Marius Boro
e50b1eb359 Update no.php (#50)
* Update no.php

* Update no.php

* Update no.php

* Update no.php

* Update no.php

* Update no.php
2018-09-04 17:25:52 +02:00
Bernd Bestel
92e0245387 Merge pull request #49 from BlizzWave/patch-14
Update it.php
2018-09-04 08:54:04 +02:00
Bernd Bestel
67d0d3c3d6 Merge pull request #48 from BlizzWave/patch-13
Update de.php
2018-09-04 08:53:54 +02:00
Bernd Bestel
23bcbc23e9 Merge pull request #47 from BlizzWave/patch-12
Update batteriesoverview.js
2018-09-04 08:53:44 +02:00
Marius Boro
085d9a0bc7 Update no.php (#46)
* Update no.php

Updated for version 1.18.0 some typos fixed and more fluent translations.

* Update no.php

* Update no.php
2018-09-04 08:53:28 +02:00
Marius Boro
368df142cf Update it.php
typo
2018-09-03 22:09:37 +02:00
Marius Boro
d38edabb14 Update de.php
typo
2018-09-03 22:09:02 +02:00
Marius Boro
4426a10e2e Update batteriesoverview.js
typo
2018-09-03 22:07:05 +02:00
Bernd Bestel
931dc9d243 Reset the date shortcut checkbox on value changes when it's not the shortcut value 2018-08-18 08:14:26 +02:00
134 changed files with 4283 additions and 1079 deletions

6
.dockerignore Normal file
View File

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

58
Dockerfile-grocy Normal file
View File

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

32
Dockerfile-grocy-nginx Normal file
View File

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

View File

@@ -2,10 +2,11 @@
ERP beyond your fridge
## Give it a try
Public demo of the latest version &rarr; [https://demo.grocy.info](https://demo.grocy.info)
- Public demo of the latest stable version &rarr; [https://demo.grocy.info](https://demo.grocy.info)
- Public demo of the latest pre-release version (current master branch) &rarr; [https://demo-prerelease.grocy.info](https://demo-prerelease.grocy.info)
## Motivation
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. ERP your fridge!
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge!
## How to install
> **NEW**
@@ -22,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`.

View File

@@ -10,4 +10,4 @@ del "%releasePath%\grocy_%version%.zip"
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!package.json -xr!yarn.lock -xr!publication_assets
"build_tools\7za.exe" a "%releasePath%\grocy_%version%.zip" "%projectPath%\public\.htaccess"
"build_tools\7za.exe" rn "%releasePath%\grocy_%version%.zip" .htaccess public\.htaccess
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\sessions data\viewcache\*
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\storage data\viewcache\*

177
composer.lock generated
View File

@@ -159,27 +159,27 @@
},
{
"name": "illuminate/container",
"version": "v5.6.33",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
"reference": "1f0757cae8749400aeda730f6438a081fc3c082d"
"reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/container/zipball/1f0757cae8749400aeda730f6438a081fc3c082d",
"reference": "1f0757cae8749400aeda730f6438a081fc3c082d",
"url": "https://api.github.com/repos/illuminate/container/zipball/0fc33b14ae6cf9a1e694fd43f2a274e590a824b2",
"reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2",
"shasum": ""
},
"require": {
"illuminate/contracts": "5.6.*",
"illuminate/contracts": "5.7.*",
"php": "^7.1.3",
"psr/container": "~1.0"
"psr/container": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.7-dev"
}
},
"autoload": {
@@ -199,31 +199,31 @@
],
"description": "The Illuminate Container package.",
"homepage": "https://laravel.com",
"time": "2018-05-24T13:16:56+00:00"
"time": "2018-05-28T08:50:10+00:00"
},
{
"name": "illuminate/contracts",
"version": "v5.6.33",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "2c029101285f6066f45e3ae5910b1b5f900fdcb4"
"reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/2c029101285f6066f45e3ae5910b1b5f900fdcb4",
"reference": "2c029101285f6066f45e3ae5910b1b5f900fdcb4",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/2daf3c078610f744e2a4dc2f44fb5060cce9835b",
"reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"psr/container": "~1.0",
"psr/simple-cache": "~1.0"
"psr/container": "^1.0",
"psr/simple-cache": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.7-dev"
}
},
"autoload": {
@@ -243,32 +243,32 @@
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"time": "2018-07-31T12:49:53+00:00"
"time": "2018-09-18T12:50:05+00:00"
},
{
"name": "illuminate/events",
"version": "v5.6.33",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
"reference": "5bdd8e84c0528970961289da088306c632eca8f7"
"reference": "4cf622acc05592f86d4a5c77ad1a544d38e58dee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/events/zipball/5bdd8e84c0528970961289da088306c632eca8f7",
"reference": "5bdd8e84c0528970961289da088306c632eca8f7",
"url": "https://api.github.com/repos/illuminate/events/zipball/4cf622acc05592f86d4a5c77ad1a544d38e58dee",
"reference": "4cf622acc05592f86d4a5c77ad1a544d38e58dee",
"shasum": ""
},
"require": {
"illuminate/container": "5.6.*",
"illuminate/contracts": "5.6.*",
"illuminate/support": "5.6.*",
"illuminate/container": "5.7.*",
"illuminate/contracts": "5.7.*",
"illuminate/support": "5.7.*",
"php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.7-dev"
}
},
"autoload": {
@@ -288,39 +288,39 @@
],
"description": "The Illuminate Events package.",
"homepage": "https://laravel.com",
"time": "2018-07-23T01:01:28+00:00"
"time": "2018-07-26T15:27:42+00:00"
},
{
"name": "illuminate/filesystem",
"version": "v5.6.33",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
"reference": "97e2f19e2c2ec74779431acedfb746d9054da33a"
"reference": "a09fae4470494dc9867609221b46fe844f2f3b70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/filesystem/zipball/97e2f19e2c2ec74779431acedfb746d9054da33a",
"reference": "97e2f19e2c2ec74779431acedfb746d9054da33a",
"url": "https://api.github.com/repos/illuminate/filesystem/zipball/a09fae4470494dc9867609221b46fe844f2f3b70",
"reference": "a09fae4470494dc9867609221b46fe844f2f3b70",
"shasum": ""
},
"require": {
"illuminate/contracts": "5.6.*",
"illuminate/support": "5.6.*",
"illuminate/contracts": "5.7.*",
"illuminate/support": "5.7.*",
"php": "^7.1.3",
"symfony/finder": "~4.0"
"symfony/finder": "^4.1"
},
"suggest": {
"league/flysystem": "Required to use the Flysystem local and FTP drivers (~1.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
"league/flysystem-cached-adapter": "Required to use the Flysystem cache (~1.0).",
"league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
"league/flysystem-sftp": "Required to use the Flysystem SFTP driver (~1.0)."
"league/flysystem": "Required to use the Flysystem local and FTP drivers (^1.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
"league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
"league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (^1.0).",
"league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.7-dev"
}
},
"autoload": {
@@ -340,42 +340,43 @@
],
"description": "The Illuminate Filesystem package.",
"homepage": "https://laravel.com",
"time": "2018-07-29T15:22:18+00:00"
"time": "2018-08-14T19:42:44+00:00"
},
{
"name": "illuminate/support",
"version": "v5.6.33",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "0561e4e48797fbaeafeec0054b14605b08722a5a"
"reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/0561e4e48797fbaeafeec0054b14605b08722a5a",
"reference": "0561e4e48797fbaeafeec0054b14605b08722a5a",
"url": "https://api.github.com/repos/illuminate/support/zipball/f7c68e8c8aab200cc8ad84f974d5511cda58a742",
"reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742",
"shasum": ""
},
"require": {
"doctrine/inflector": "~1.1",
"doctrine/inflector": "^1.1",
"ext-mbstring": "*",
"illuminate/contracts": "5.6.*",
"nesbot/carbon": "^1.24.1",
"illuminate/contracts": "5.7.*",
"nesbot/carbon": "^1.26.3",
"php": "^7.1.3"
},
"conflict": {
"tightenco/collect": "<5.5.33"
},
"suggest": {
"illuminate/filesystem": "Required to use the composer class (5.6.*).",
"illuminate/filesystem": "Required to use the composer class (5.7.*).",
"moontoast/math": "Required to use ordered UUIDs (^1.1).",
"ramsey/uuid": "Required to use Str::uuid() (^3.7).",
"symfony/process": "Required to use the composer class (~4.0).",
"symfony/var-dumper": "Required to use the dd function (~4.0)."
"symfony/process": "Required to use the composer class (^4.1).",
"symfony/var-dumper": "Required to use the dd function (^4.1)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.7-dev"
}
},
"autoload": {
@@ -398,35 +399,35 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2018-08-06T19:58:11+00:00"
"time": "2018-09-19T18:36:57+00:00"
},
{
"name": "illuminate/view",
"version": "v5.6.33",
"version": "v5.7.6",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
"reference": "8d4e1c4d8c133eaca33c94ee35b7c0d2ef1dc66f"
"reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/view/zipball/8d4e1c4d8c133eaca33c94ee35b7c0d2ef1dc66f",
"reference": "8d4e1c4d8c133eaca33c94ee35b7c0d2ef1dc66f",
"url": "https://api.github.com/repos/illuminate/view/zipball/3ccd29550afe61eb02ad9e4bae0c2e661aadd7af",
"reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af",
"shasum": ""
},
"require": {
"illuminate/container": "5.6.*",
"illuminate/contracts": "5.6.*",
"illuminate/events": "5.6.*",
"illuminate/filesystem": "5.6.*",
"illuminate/support": "5.6.*",
"illuminate/container": "5.7.*",
"illuminate/contracts": "5.7.*",
"illuminate/events": "5.7.*",
"illuminate/filesystem": "5.7.*",
"illuminate/support": "5.7.*",
"php": "^7.1.3",
"symfony/debug": "~4.0"
"symfony/debug": "^4.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.6-dev"
"dev-master": "5.7-dev"
}
},
"autoload": {
@@ -446,7 +447,7 @@
],
"description": "The Illuminate View package.",
"homepage": "https://laravel.com",
"time": "2018-07-19T23:06:53+00:00"
"time": "2018-09-18T12:50:05+00:00"
},
{
"name": "morris/lessql",
@@ -553,16 +554,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.33.0",
"version": "1.34.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "55667c1007a99e82030874b1bb14d24d07108413"
"reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/55667c1007a99e82030874b1bb14d24d07108413",
"reference": "55667c1007a99e82030874b1bb14d24d07108413",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33",
"reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33",
"shasum": ""
},
"require": {
@@ -604,7 +605,7 @@
"datetime",
"time"
],
"time": "2018-08-07T08:39:47+00:00"
"time": "2018-09-20T19:36:25+00:00"
},
{
"name": "nikic/fast-route",
@@ -1095,16 +1096,16 @@
},
{
"name": "slim/slim",
"version": "3.10.0",
"version": "3.11.0",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748"
"reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
"reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a",
"reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a",
"shasum": ""
},
"require": {
@@ -1162,20 +1163,20 @@
"micro",
"router"
],
"time": "2018-04-19T19:29:08+00:00"
"time": "2018-09-16T10:54:21+00:00"
},
{
"name": "symfony/debug",
"version": "v4.1.3",
"version": "v4.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "9316545571f079c4dd183e674721d9dc783ce196"
"reference": "b4a0b67dee59e2cae4449a8f8eabc508d622fd33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/9316545571f079c4dd183e674721d9dc783ce196",
"reference": "9316545571f079c4dd183e674721d9dc783ce196",
"url": "https://api.github.com/repos/symfony/debug/zipball/b4a0b67dee59e2cae4449a8f8eabc508d622fd33",
"reference": "b4a0b67dee59e2cae4449a8f8eabc508d622fd33",
"shasum": ""
},
"require": {
@@ -1218,20 +1219,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-07-26T11:24:31+00:00"
"time": "2018-09-22T19:04:12+00:00"
},
{
"name": "symfony/finder",
"version": "v4.1.3",
"version": "v4.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068"
"reference": "f0b042d445c155501793e7b8007457f9f5bb1c8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068",
"reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068",
"url": "https://api.github.com/repos/symfony/finder/zipball/f0b042d445c155501793e7b8007457f9f5bb1c8c",
"reference": "f0b042d445c155501793e7b8007457f9f5bb1c8c",
"shasum": ""
},
"require": {
@@ -1267,7 +1268,7 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2018-07-26T11:24:31+00:00"
"time": "2018-09-21T12:49:42+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@@ -1330,16 +1331,16 @@
},
{
"name": "symfony/translation",
"version": "v4.1.3",
"version": "v4.1.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "6fcd1bd44fd6d7181e6ea57a6f4e08a09b29ef65"
"reference": "6e49130ddf150b7bfe9e34edb2f3f698aa1aa43b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/6fcd1bd44fd6d7181e6ea57a6f4e08a09b29ef65",
"reference": "6fcd1bd44fd6d7181e6ea57a6f4e08a09b29ef65",
"url": "https://api.github.com/repos/symfony/translation/zipball/6e49130ddf150b7bfe9e34edb2f3f698aa1aa43b",
"reference": "6e49130ddf150b7bfe9e34edb2f3f698aa1aa43b",
"shasum": ""
},
"require": {
@@ -1395,7 +1396,7 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-07-26T11:24:31+00:00"
"time": "2018-09-21T12:49:42+00:00"
},
{
"name": "tuupola/callable-handler",

View File

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

View File

@@ -5,6 +5,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\DatabaseService;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\LocalizationService;
use \Grocy\Services\UsersService;
class BaseController
{
@@ -15,10 +16,21 @@ class BaseController
$localizationService = new LocalizationService(GROCY_CULTURE);
$this->LocalizationService = $localizationService;
$applicationService = new ApplicationService();
$versionInfo = $applicationService->GetInstalledVersion();
$container->view->set('version', $versionInfo->Version);
$container->view->set('releaseDate', $versionInfo->ReleaseDate);
if (GROCY_MODE === 'prerelease')
{
$commitHash = trim(exec('git log --pretty="%h" -n1 HEAD'));
$commitDate = trim(exec('git log --date=iso --pretty="%cd" -n1 HEAD'));
$container->view->set('version', "pre-release-$commitHash");
$container->view->set('releaseDate', \substr($commitDate, 0, 19));
}
else
{
$applicationService = new ApplicationService();
$versionInfo = $applicationService->GetInstalledVersion();
$container->view->set('version', $versionInfo->Version);
$container->view->set('releaseDate', $versionInfo->ReleaseDate);
}
$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations());
$container->view->set('L', function($text, ...$placeholderValues) use($localizationService)
@@ -30,6 +42,15 @@ class BaseController
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
});
try {
$usersService = new UsersService();
$container->view->set('userSettings', $usersService->GetUserSettings(GROCY_USER_ID));
}
catch (\Exception $ex)
{
// Happens when database is not initialised or migrated...
}
$this->AppContainer = $container;
}

View File

@@ -2,19 +2,19 @@
namespace Grocy\Controllers;
use \Grocy\Services\HabitsService;
use \Grocy\Services\ChoresService;
class HabitsApiController extends BaseApiController
class ChoresApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->HabitsService = new HabitsService();
$this->ChoresService = new ChoresService();
}
protected $HabitsService;
protected $ChoresService;
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
public function TrackChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$trackedTime = date('Y-m-d H:i:s');
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']) && IsIsoDateTime($request->getQueryParams()['tracked_time']))
@@ -30,7 +30,7 @@ class HabitsApiController extends BaseApiController
try
{
$this->HabitsService->TrackHabit($args['habitId'], $trackedTime, $doneBy);
$this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
@@ -39,11 +39,11 @@ class HabitsApiController extends BaseApiController
}
}
public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
public function ChoreDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId']));
return $this->ApiResponse($this->ChoresService->GetChoreDetails($args['choreId']));
}
catch (\Exception $ex)
{
@@ -53,6 +53,6 @@ class HabitsApiController extends BaseApiController
public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->HabitsService->GetCurrent());
return $this->ApiResponse($this->ChoresService->GetCurrent());
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\ChoresService;
class ChoresController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->ChoresService = new ChoresService();
}
protected $ChoresService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'choresoverview', [
'chores' => $this->Database->chores()->orderBy('name'),
'currentChores' => $this->ChoresService->GetCurrent(),
'nextXDays' => 5
]);
}
public function TrackChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'choretracking', [
'chores' => $this->Database->chores()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
public function ChoresList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'chores', [
'chores' => $this->Database->chores()->orderBy('name')
]);
}
public function Analysis(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'choresanalysis', [
'choresLog' => $this->Database->chores_log()->orderBy('tracked_time', 'DESC'),
'chores' => $this->Database->chores()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
public function ChoreEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['choreId'] == 'new')
{
return $this->AppContainer->view->render($response, 'choreform', [
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'),
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'choreform', [
'chore' => $this->Database->chores($args['choreId']),
'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'),
'mode' => 'edit'
]);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\FilesService;
class FilesApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->FilesService = new FilesService();
}
protected $FilesService;
public function Upload(\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');
}
$data = $request->getBody()->getContents();
file_put_contents($this->FilesService->GetFilePath($args['group'], $fileName), $data);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -1,68 +0,0 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\HabitsService;
class HabitsController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->HabitsService = new HabitsService();
}
protected $HabitsService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habitsoverview', [
'habits' => $this->Database->habits()->orderBy('name'),
'currentHabits' => $this->HabitsService->GetCurrent(),
'nextXDays' => 5
]);
}
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habittracking', [
'habits' => $this->Database->habits()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
public function HabitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habits', [
'habits' => $this->Database->habits()->orderBy('name')
]);
}
public function Analysis(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habitsanalysis', [
'habitsLog' => $this->Database->habits_log()->orderBy('tracked_time', 'DESC'),
'habits' => $this->Database->habits()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
public function HabitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['habitId'] == 'new')
{
return $this->AppContainer->view->render($response, 'habitform', [
'periodTypes' => GetClassConstants('\Grocy\Services\HabitsService'),
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'habitform', [
'habit' => $this->Database->habits($args['habitId']),
'periodTypes' => GetClassConstants('\Grocy\Services\HabitsService'),
'mode' => 'edit'
]);
}
}
}

View File

@@ -25,11 +25,12 @@ class LoginController extends BaseController
{
$user = $this->Database->users()->where('username', $postParams['username'])->fetch();
$inputPassword = $postParams['password'];
$stayLoggedInPermanently = $postParams['stay_logged_in'] == 'on';
if ($user !== null && password_verify($inputPassword, $user->password))
{
$sessionKey = $this->SessionService->CreateSession($user->id);
setcookie($this->SessionCookieName, $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService
$sessionKey = $this->SessionService->CreateSession($user->id, $stayLoggedInPermanently);
setcookie($this->SessionCookieName, $sessionKey, time() + 31220640000); // Cookie expires in 999 years, but session validity is up to SessionService
if (password_needs_rehash($user->password, PASSWORD_DEFAULT))
{

View File

@@ -54,7 +54,8 @@ class StockController extends BaseController
'listItems' => $this->Database->shopping_list(),
'products' => $this->Database->products()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'missingProducts' => $this->StockService->GetMissingProducts()
'missingProducts' => $this->StockService->GetMissingProducts(),
'productGroups' => $this->Database->product_groups()->orderBy('name')
]);
}
@@ -63,7 +64,8 @@ class StockController extends BaseController
return $this->AppContainer->view->render($response, 'products', [
'products' => $this->Database->products()->orderBy('name'),
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name')
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productGroups' => $this->Database->product_groups()->orderBy('name')
]);
}
@@ -74,6 +76,13 @@ class StockController extends BaseController
]);
}
public function ProductGroupsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'productgroups', [
'productGroups' => $this->Database->product_groups()->orderBy('name')
]);
}
public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'quantityunits', [
@@ -88,6 +97,7 @@ class StockController extends BaseController
return $this->AppContainer->view->render($response, 'productform', [
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productgroups' => $this->Database->product_groups()->orderBy('name'),
'mode' => 'create'
]);
}
@@ -97,6 +107,7 @@ class StockController extends BaseController
'product' => $this->Database->products($args['productId']),
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'productgroups' => $this->Database->product_groups()->orderBy('name'),
'mode' => 'edit'
]);
}
@@ -119,6 +130,23 @@ class StockController extends BaseController
}
}
public function ProductGroupEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['productGroupId'] == 'new')
{
return $this->AppContainer->view->render($response, 'productgroupform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'productgroupform', [
'group' => $this->Database->product_groups($args['productGroupId']),
'mode' => 'edit'
]);
}
}
public function QuantityUnitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['quantityunitId'] == 'new')

View File

@@ -0,0 +1,41 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\DatabaseService;
class SystemApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->DatabaseService = new DatabaseService();
}
protected $DatabaseService;
public function GetDbChangedTime(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse(array(
'changed_time' => $this->DatabaseService->GetDbChangedTime()
));
}
public function LogMissingLocalization(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if (GROCY_MODE === 'dev')
{
try
{
$requestBody = $request->getParsedBody();
$this->LocalizationService->LogMissingLocalization(GROCY_CULTURE, $requestBody['text']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\TasksService;
class TasksApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->TasksService = new TasksService();
}
protected $TasksService;
public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->TasksService->GetCurrent());
}
public function MarkTaskAsCompleted(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$doneTime = date('Y-m-d H:i:s');
if (isset($request->getQueryParams()['done_time']) && !empty($request->getQueryParams()['done_time']) && IsIsoDateTime($request->getQueryParams()['done_time']))
{
$doneTime = $request->getQueryParams()['done_time'];
}
try
{
$this->TasksService->MarkTaskAsCompleted($args['taskId'], $doneTime);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\TasksService;
class TasksController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->TasksService = new TasksService();
}
protected $TasksService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if (isset($request->getQueryParams()['include_done']))
{
$tasks = $this->Database->tasks()->orderBy('name');
}
else
{
$tasks = $this->TasksService->GetCurrent();
}
return $this->AppContainer->view->render($response, 'tasks', [
'tasks' => $tasks,
'nextXDays' => 5,
'taskCategories' => $this->Database->task_categories()->orderBy('name'),
'users' => $this->Database->users()
]);
}
public function TaskEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['taskId'] == 'new')
{
return $this->AppContainer->view->render($response, 'taskform', [
'mode' => 'create',
'taskCategories' => $this->Database->task_categories()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
else
{
return $this->AppContainer->view->render($response, 'taskform', [
'task' => $this->Database->tasks($args['taskId']),
'mode' => 'edit',
'taskCategories' => $this->Database->task_categories()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
}
public function TaskCategoriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'taskcategories', [
'taskCategories' => $this->Database->task_categories()->orderBy('name')
]);
}
public function TaskCategoryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['categoryId'] == 'new')
{
return $this->AppContainer->view->render($response, 'taskcategoryform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'taskcategoryform', [
'category' => $this->Database->task_categories($args['categoryId']),
'mode' => 'edit'
]);
}
}
}

View File

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

30
docker-compose.yml Normal file
View File

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

28
docker_nginx/common.conf Normal file
View File

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

View File

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

View File

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

42
docker_nginx/nginx.conf Normal file
View File

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

View File

@@ -24,6 +24,67 @@
}
],
"paths": {
"/system/get-db-changed-time": {
"get": {
"description": "Returns the time when the database was last changed",
"tags": [
"System"
],
"responses": {
"200": {
"description": "An DbChangedTimeResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DbChangedTimeResponse"
}
}
}
}
}
}
},
"/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",
@@ -54,7 +115,7 @@
"$ref": "#/components/schemas/Product"
},
{
"$ref": "#/components/schemas/Habit"
"$ref": "#/components/schemas/Chore"
},
{
"$ref": "#/components/schemas/Battery"
@@ -128,7 +189,7 @@
"$ref": "#/components/schemas/Product"
},
{
"$ref": "#/components/schemas/Habit"
"$ref": "#/components/schemas/Chore"
},
{
"$ref": "#/components/schemas/Battery"
@@ -191,7 +252,7 @@
"$ref": "#/components/schemas/Product"
},
{
"$ref": "#/components/schemas/Habit"
"$ref": "#/components/schemas/Chore"
},
{
"$ref": "#/components/schemas/Battery"
@@ -274,7 +335,7 @@
"$ref": "#/components/schemas/Product"
},
{
"$ref": "#/components/schemas/Habit"
"$ref": "#/components/schemas/Chore"
},
{
"$ref": "#/components/schemas/Battery"
@@ -370,6 +431,66 @@
}
}
},
"/files/upload/{group}": {
"post": {
"description": "Uploads a single file to /data/storage/{group}/{file_name}",
"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"
}
}
],
"requestBody": {
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"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": {
"get": {
"description": "Returns all users",
@@ -537,6 +658,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",
@@ -1041,18 +1253,18 @@
}
}
},
"/habits/track-habit-execution/{habitId}": {
"/chores/track-chore-execution/{choreId}": {
"get": {
"description": "Tracks an execution of the given habit",
"description": "Tracks an execution of the given chore",
"tags": [
"Habits"
"Chores"
],
"parameters": [
{
"in": "path",
"name": "habitId",
"name": "choreId",
"required": true,
"description": "A valid habit id",
"description": "A valid chore id",
"schema": {
"type": "integer"
}
@@ -1061,7 +1273,7 @@
"in": "query",
"name": "tracked_time",
"required": false,
"description": "The time of when the habit was executed, when omitted, the current time is used",
"description": "The time of when the chore was executed, when omitted, the current time is used",
"schema": {
"type": "date-time"
}
@@ -1070,7 +1282,7 @@
"in": "query",
"name": "done_by",
"required": false,
"description": "A valid user id of who executed this habit, when omitted, the currently authenticated user will be used",
"description": "A valid user id of who executed this chore, when omitted, the currently authenticated user will be used",
"schema": {
"type": "integer"
}
@@ -1088,7 +1300,7 @@
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Not existing habit)",
"description": "A VoidApiActionResponse object (possible errors are: Not existing chore)",
"content": {
"application/json": {
"schema": {
@@ -1100,18 +1312,18 @@
}
}
},
"/habits/get-habit-details/{habitId}": {
"/chores/get-chore-details/{choreId}": {
"get": {
"description": "Returns details of the given habit",
"description": "Returns details of the given chore",
"tags": [
"Habits"
"Chores"
],
"parameters": [
{
"in": "path",
"name": "habitId",
"name": "choreId",
"required": true,
"description": "A valid habit id",
"description": "A valid chore id",
"schema": {
"type": "integer"
}
@@ -1119,17 +1331,17 @@
],
"responses": {
"200": {
"description": "A HabitDetailsResponse object",
"description": "A ChoreDetailsResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HabitDetailsResponse"
"$ref": "#/components/schemas/ChoreDetailsResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Not existing habit)",
"description": "A VoidApiActionResponse object (possible errors are: Not existing chore)",
"content": {
"application/json": {
"schema": {
@@ -1141,21 +1353,21 @@
}
}
},
"/habits/get-current": {
"/chores/get-current": {
"get": {
"description": "Returns all habits incl. the next estimated execution time per habit",
"description": "Returns all chores incl. the next estimated execution time per chore",
"tags": [
"Habits"
"Chores"
],
"responses": {
"200": {
"description": "An array of CurrentHabitResponse objects",
"description": "An array of CurrentChoreResponse objects",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CurrentHabitResponse"
"$ref": "#/components/schemas/CurrentChoreResponse"
}
}
}
@@ -1278,6 +1490,79 @@
}
}
}
},
"/tasks/get-current": {
"get": {
"description": "Returns all tasks which are not done yet",
"tags": [
"Tasks"
],
"responses": {
"200": {
"description": "An array of Task objects",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Task"
}
}
}
}
}
}
}
},
"/tasks/mark-task-as-completed/{taskId}": {
"get": {
"description": "Marks the given task as completed",
"tags": [
"Tasks"
],
"parameters": [
{
"in": "path",
"name": "taskId",
"required": true,
"description": "A valid task id",
"schema": {
"type": "integer"
}
},
{
"in": "query",
"name": "done_time",
"required": false,
"description": "The time of when the task was completed, when omitted, the current time is used",
"schema": {
"type": "date-time"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Not existing task)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
}
},
"components": {
@@ -1286,13 +1571,16 @@
"type": "string",
"enum": [
"products",
"habits",
"chores",
"batteries",
"locations",
"quantity_units",
"shopping_list",
"recipes",
"recipes_pos"
"recipes_pos",
"tasks",
"task_categories",
"product_groups"
]
},
"StockTransactionType": {
@@ -1494,20 +1782,20 @@
}
}
},
"HabitDetailsResponse": {
"ChoreDetailsResponse": {
"type": "object",
"properties": {
"habit": {
"$ref": "#/components/schemas/Habit"
"chore": {
"$ref": "#/components/schemas/Chore"
},
"last_tracked": {
"type": "string",
"format": "date-time",
"description": "When this habit was last tracked"
"description": "When this chore was last tracked"
},
"track_count": {
"type": "integer",
"description": "How often this habit was tracked so far"
"description": "How often this chore was tracked so far"
},
"last_done_by": {
"$ref": "#/components/schemas/UserDto"
@@ -1521,7 +1809,7 @@
"BatteryDetailsResponse": {
"type": "object",
"properties": {
"habit": {
"chore": {
"$ref": "#/components/schemas/Battery"
},
"last_charged": {
@@ -1709,7 +1997,7 @@
}
}
},
"Habit": {
"Chore": {
"type": "object",
"properties": {
"id": {
@@ -1737,13 +2025,13 @@
}
}
},
"HabitLogEntry": {
"ChoreLogEntry": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"habit_id": {
"chore_id": {
"type": "integer"
},
"tracked_time": {
@@ -1842,10 +2130,10 @@
}
}
},
"CurrentHabitResponse": {
"CurrentChoreResponse": {
"type": "object",
"properties": {
"habit_id": {
"chore_id": {
"type": "integer"
},
"last_tracked_time": {
@@ -1855,7 +2143,7 @@
"next_estimated_execution_time": {
"type": "string",
"format": "date-time",
"description": "The next estimated execution time of this habit, 2999-12-31 23:59:59 when the given habit has a period_type of manually"
"description": "The next estimated execution time of this chore, 2999-12-31 23:59:59 when the given chore has a period_type of manually"
}
}
},
@@ -1898,6 +2186,66 @@
}
}
}
},
"Task": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"due_date": {
"type": "string",
"format": "date-time"
},
"done": {
"type": "integer"
},
"done_timestamp": {
"type": "string",
"format": "date-time"
},
"category_id": {
"type": "integer"
},
"assigned_to_user_id": {
"type": "integer"
},
"row_created_timestamp": {
"type": "string",
"format": "date-time"
}
}
},
"DbChangedTimeResponse": {
"type": "object",
"properties": {
"changed_time": {
"type": "string",
"format": "date-time"
}
}
},
"UserSetting": {
"type": "object",
"properties": {
"value": {
"type": "string"
}
}
},
"MissingLocalizationRequest": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
}
}
},
"examples": {

View File

@@ -32,6 +32,11 @@ class UrlManager
private function GetBaseUrl()
{
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false)
{
$_SERVER['HTTPS'] = 'on';
}
return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
}
}

View File

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

6
info.php Normal file
View File

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

View File

@@ -9,20 +9,20 @@ return array(
'Amount' => 'Menge',
'Next best before date' => 'Nächstes MHD',
'Logout' => 'Abmelden',
'Habits overview' => 'Gewohnheiten',
'Chores overview' => 'Hausarbeiten',
'Batteries overview' => 'Batterien',
'Purchase' => 'Einkauf',
'Consume' => 'Verbrauch',
'Inventory' => 'Inventur',
'Shopping list' => 'Einkaufszettel',
'Habit tracking' => 'Gewohnheit-Ausführung',
'Chore tracking' => 'Hausarbeiten-Ausführung',
'Battery tracking' => 'Batterie-Ladzyklus',
'Products' => 'Produkte',
'Locations' => 'Standorte',
'Quantity units' => 'Mengeneinheiten',
'Habits' => 'Gewohnheiten',
'Chores' => 'Hausarbeiten',
'Batteries' => 'Batterien',
'Habit' => 'Gewohnheit',
'Chore' => 'Hausarbeit',
'Next estimated tracking' => 'Nächste geplante Ausführung',
'Last tracked' => 'Zuletzt ausgeführt',
'Battery' => 'Batterie',
@@ -41,7 +41,7 @@ return array(
'New amount' => 'Neue Menge',
'Note' => 'Notiz',
'Tracked time' => 'Ausführungszeit',
'Habit overview' => 'Gewohnheit Übersicht',
'Chore overview' => 'Hausarbeit Übersicht',
'Tracked count' => 'Ausführungsanzahl',
'Battery overview' => 'Batterie Übersicht',
'Charge cycles count' => 'Ladezyklen',
@@ -68,11 +68,11 @@ return array(
'Create quantity unit' => 'Mengeneinheit erstellen',
'Period type' => 'Periodentyp',
'Period days' => 'Tage/Periode',
'Create habit' => 'Gewohnheit erstellen',
'Create chore' => 'Hausarbeit erstellen',
'Used in' => 'Benutzt in',
'Create battery' => 'Batterie erstellen',
'Edit battery' => 'Batterie bearbeiten',
'Edit habit' => 'Gewohnheit bearbeiten',
'Edit chore' => 'Hausarbeit bearbeiten',
'Edit quantity unit' => 'Mengeneinheit bearbeiten',
'Edit product' => 'Produkt bearbeiten',
'Edit location' => 'Standort bearbeiten',
@@ -90,7 +90,7 @@ return array(
'Are you sure to delete battery "#1"?' => 'Battery "#1" wirklich löschen?',
'Yes' => 'Ja',
'No' => 'Nein',
'Are you sure to delete habit "#1"?' => 'Gewohnheit "#1" wirklich löschen?',
'Are you sure to delete chore "#1"?' => 'Hausarbeit "#1" wirklich löschen?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" konnte nicht zu einem Produkt aufgelöst werden, wie möchtest du weiter machen?',
'Create or assign product' => 'Produkt erstellen oder verknüpfen',
'Cancel' => 'Abbrechen',
@@ -110,29 +110,29 @@ return array(
'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig',
'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt',
'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt',
'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird',
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Hausarbeit #1 Tage nach der letzten Ausführung geplant wird',
'Removed #1 #2 of #3 from stock' => '#1 #2 #3 aus dem Bestand entfernt',
'About grocy' => 'Über grocy',
'Close' => 'Schließen',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batterien müssen in den nächsten #2 Tagen geladen werden',
'#1 batteries are overdue to be charged' => '#1 Batterien sind überfällig',
'#1 habits are due to be done within the next #2 days' => '#1 Gewohnheiten stehen in den nächsten #2 Tagen an',
'#1 habits are overdue to be done' => '#1 Gewohnheiten sind überfällig',
'#1 chores are due to be done within the next #2 days' => '#1 Hausarbeiten stehen in den nächsten #2 Tagen an',
'#1 chores are overdue to be done' => '#1 Hausarbeiten sind überfällig',
'Released on' => 'Veröffentlicht am',
'Consume #3 #1 of #2' => 'Verbrauche #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 dem Bestand hinzugefügt',
'Stock amount of #1 is now #2 #3' => 'Es sind nun #2 #3 #1 im Bestand',
'Tracked execution of habit #1 on #2' => 'Ausführung von #1 am #2 erfasst',
'Tracked charge cylce of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst',
'Tracked execution of chore #1 on #2' => 'Ausführung von #1 am #2 erfasst',
'Tracked charge cycle of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst',
'Consume all #1 which are currently in stock' => 'Verbrauche den kompletten Bestand von #1',
'All' => 'Alle',
'Track charge cycle of battery #1' => 'Erfasse einen Ladezyklus für Batterie #1',
'Track execution of habit #1' => 'Erfasse eine Ausführung von #1',
'Track execution of chore #1' => 'Erfasse eine Ausführung von #1',
'Filter by location' => 'Nach Standort filtern',
'Search' => 'Suche',
'Not logged in' => 'Nicht angemeldet',
'You have to select a product' => 'Ein Produkt muss ausgewählt werden',
'You have to select a habit' => 'Eine Gewohnheit muss ausgewählt werden',
'You have to select a chore' => 'Eine Hausarbeit muss ausgewählt werden',
'You have to select a battery' => 'Eine Batterie muss ausgewählt werden',
'A name is required' => 'Ein Name ist erforderlich',
'A location is required' => 'Ein Standort ist erforderlich',
@@ -183,8 +183,8 @@ return array(
'Done by' => 'Ausgeführt von',
'Last done by' => 'Zuletzt ausgeführt von',
'Unknown' => 'Unbekannt',
'Filter by habit' => 'Nach Gewohnheit filtern',
'Habits analysis' => 'Gewohnheiten Analyse',
'Filter by chore' => 'Nach Hausarbeit filtern',
'Chores analysis' => 'Hausarbeiten Analyse',
'0 means suggestions for the next charge cycle are disabled' => '0 bedeutet dass Vorschläge für den nächsten Ladezyklus deaktiviert sind',
'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)',
'Last price' => 'Letzter Preis',
@@ -198,8 +198,8 @@ return array(
'#1 product is below defined min. stock amount' => '#1 Produkt ist unter Mindestbestand',
'Unit' => 'Einheit',
'Units' => 'Einheiten',
'#1 habit is due to be done within the next #2 days' => '#1 Gewohnheit steht in den nächsten #2 Tagen an',
'#1 habit is overdue to be done' => '#1 Gewohnheit ist überfällig',
'#1 chore is due to be done within the next #2 days' => '#1 Hausarbeit steht in den nächsten #2 Tagen an',
'#1 chore is overdue to be done' => '#1 Hausarbeit ist überfällig',
'#1 battery is due to be charged within the next #2 days' => '#1 Batterie muss in den nächsten #2 Tagen geladen werden',
'#1 battery is overdue to be charged' => '#1 Batterie ist überfällig',
'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 Einheit wurde automatisch hinzugefügt und gilt zusätzlich der hier eingegebenen Menge',
@@ -213,6 +213,50 @@ return array(
'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)?' => 'Sicher, dass alle Zutaten die vom Rezept "#1" benötigt werden aus dem Bestand entfernt werden sollen (Zutaten markiert mit "nur prüfen, ob eine einzelne Einheit vorrätig ist" werden ignoriert)?',
'Removed all ingredients of recipe "#1" from stock' => 'Alle Zutaten, die vom Rezept "#1" benötigt werden, wurdem aus dem Bestand entfernt',
'Consume all ingredients needed by this recipe' => 'Alle Zutaten, die von diesem Rezept benötigt werden, aus dem Bestand enternen',
'Click to show technical details' => 'Klick um technische Details anzuzeigen',
'Error while saving, probably this item already exists' => 'Fehler beim Speichern, möglicherweise existiert das Element bereits',
'Error details' => 'Fehlerdetails',
'Tasks' => 'Aufgaben',
'Show done tasks' => 'Erledigte Aufgaben anzeigen',
'Task' => 'Aufgabe',
'Due' => 'Fällig',
'Assigned to' => 'Zugewiesen an',
'Mark task "#1" as completed' => 'Aufgabe "#1" als erledigt markieren',
'Uncategorized' => 'Nicht kategorisiert',
'Task categories' => 'Aufgabenkategorien',
'Create task' => 'Aufgabe erstellen',
'A due date is required' => 'Ein Fälligkeitsdatum ist erforderlich',
'Category' => 'Kategorie',
'Edit task' => 'Aufgabe bearbeiten',
'Are you sure to delete task "#1"?' => 'Aufgabe "#1" wirklich löschen?',
'#1 task is due to be done within the next #2 days' => '#1 Aufgabe steht in den nächsten #2 Tagen an',
'#1 tasks are due to be done within the next #2 days' => '#1 Aufgaben stehen in den nächsten #2 Tagen an',
'#1 task is overdue to be done' => '#1 Aufgabe ist überfällig',
'#1 tasks are overdue to be done' => '#1 Aufgaben sind überfällig',
'Edit task category' => 'Aufgabenkategorie bearbeiten',
'Create task category' => 'Aufgabenkategorie erstellen',
'Product groups' => 'Produktgruppen',
'Ungrouped' => 'Ungruppiert',
'Create product group' => 'Produktgruppe erstellen',
'Edit product group' => 'Produktgruppe bearbeiten',
'Product group' => 'Produktgruppe',
'Are you sure to delete product group "#1"?' => 'Produktgruppe "#1" wirklich löschen?',
'Stay logged in permanently' => 'Dauerhaft angemeldet bleiben',
'When not set, you will get logged out at latest after 30 days' => 'Wenn nicht gesetzt, wirst du spätestens nach 30 Tagen automatisch abgemeldet',
'Filter by status' => 'Nach Status filtern',
'Below min. stock amount' => 'Unter Mindestbestand',
'Expiring soon' => 'Bald ablaufend',
'Already expired' => 'Bereits abgelaufen',
'Due soon' => 'Bald fällig',
'Overdue' => 'Überfällig',
'View settings' => 'xxx',
'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',
//Constants
'manually' => 'Manuell',
@@ -282,5 +326,17 @@ return array(
'Grams' => 'Gramm',
'Flour' => 'Mehl',
'Pancakes' => 'Pfannkuchen',
'Sugar' => 'Zucker'
'Sugar' => 'Zucker',
'Home' => 'Zuhause',
'Life' => 'Leben',
'Projects' => 'Projekte',
'Repair the garage door' => 'Garagentor reparieren',
'Fork and improve grocy' => 'grocy forken und verbessern',
'Find a solution for what to do when I forget the door keys' => 'Eine Lösung für "Haustürschlüssel vergessen" finden',
'Sweets' => 'Süßigkeiten',
'Bakery products' => 'Bäckerei Produkte',
'Tinned food' => 'Konservern',
'Butchery products' => 'Metzgerei',
'Vegetables/Fruits' => 'Obst/Gemüse',
'Refrigerated products' => 'Kühlregal'
);

View File

@@ -9,20 +9,20 @@ return array(
'Amount' => 'quantità',
'Next best before date' => 'Prossima data di scadenza',
'Logout' => 'Logout',
'Habits overview' => 'Riepilogo delle abitudini',
'Chores overview' => 'Riepilogo delle abitudini',
'Batteries overview' => 'Riepilogo delle batterie',
'Purchase' => 'Acquisti',
'Consume' => 'Consumi',
'Inventory' => 'Inventario',
'Shopping list' => 'Lista della spesa',
'Habit tracking' => 'Dati abitudini',
'Chore tracking' => 'Dati abitudini',
'Battery tracking' => 'Dati batterie',
'Products' => 'Prodotti',
'Locations' => 'Posizioni',
'Quantity units' => 'Unità di misura',
'Habits' => 'Abitudini',
'Chores' => 'Abitudini',
'Batteries' => 'Batterie',
'Habit' => 'Abitudine',
'Chore' => 'Abitudine',
'Next estimated tracking' => 'Prossima esecuzione',
'Last tracked' => 'Ultima esecuzione',
'Battery' => 'Batterie',
@@ -41,7 +41,7 @@ return array(
'New amount' => 'Nuova quantità',
'Note' => 'Nota',
'Tracked time' => 'Ora di esecuzione',
'Habit overview' => 'Riepilogo dell\'abitudine',
'Chore overview' => 'Riepilogo dell\'abitudine',
'Tracked count' => 'Numero di esecuzioni',
'Battery overview' => 'Riepilogo della batteria',
'Charge cycles count' => 'Numero di ricariche',
@@ -68,11 +68,11 @@ return array(
'Create quantity unit' => 'Aggiungi unità di misura',
'Period type' => 'Tipo di ripetizione',
'Period days' => 'Periodo in giorni',
'Create habit' => 'Aggiungi abitudine',
'Create chore' => 'Aggiungi abitudine',
'Used in' => 'Usato in',
'Create battery' => 'Aggiungi batteria',
'Edit battery' => 'Modifica batteria',
'Edit habit' => 'Modifica abitudine',
'Edit chore' => 'Modifica abitudine',
'Edit quantity unit' => 'Modifica unità di misura',
'Edit product' => 'Modifica prodotto',
'Edit location' => 'Modifica posizione',
@@ -90,7 +90,7 @@ return array(
'Are you sure to delete battery "#1"?' => 'Sei sicuro di voler eliminare la batteria "#1"?',
'Yes' => 'Si',
'No' => 'No',
'Are you sure to delete habit "#1"?' => 'Sei sicuro di voler eliminare l\'abitudine "#1"?',
'Are you sure to delete chore "#1"?' => 'Sei sicuro di voler eliminare l\'abitudine "#1"?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" non è stato associato a nessun prodotto, vuoi procedere?',
'Create or assign product' => 'Aggiungi o assegna prodotto',
'Cancel' => 'Annulla',
@@ -110,29 +110,29 @@ return array(
'This product is not in stock' => 'Questo prodotto non è in dispensa',
'This means #1 will be added to stock' => '#1 sarà aggiunto alla dispensa',
'This means #1 will be removed from stock' => '#1 sarà rimosso dalla dispensa',
'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'L\'esecuzione dell\'abitudine è #1 giorni dopo la precedente',
'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'L\'esecuzione dell\'abitudine è #1 giorni dopo la precedente',
'Removed #1 #2 of #3 from stock' => '#1 #2 su #3 rimossi dalla dispensa',
'About grocy' => 'Riguardo grocy',
'Close' => 'Chiudi',
'#1 batteries are due to be charged within the next #2 days' => '#1 batterie da ricaricare entro #2 giorni',
'#1 batteries are overdue to be charged' => '#1 batterie devono essere ricaricate',
'#1 habits are due to be done within the next #2 days' => '#1 abitudini da eseguire entro #2 giorni',
'#1 habits are overdue to be done' => '#1 abitudini da eseguire',
'#1 chores are due to be done within the next #2 days' => '#1 abitudini da eseguire entro #2 giorni',
'#1 chores are overdue to be done' => '#1 abitudini da eseguire',
'Released on' => 'Rilasciato il',
'Consume #3 #1 of #2' => 'Consumati #3 #1 di #2',
'Added #1 #2 of #3 to stock' => 'Aggiunti #1 #2 di #3',
'Stock amount of #1 is now #2 #3' => 'La quantità in dispensa di #1 è ora #2 #3',
'Tracked execution of habit #1 on #2' => 'Esecuzione dell\'abitudine #1 registrata il #2',
'Tracked charge cylce of battery #1 on #2' => 'Ricarica della batteria #1 effettuata il #2',
'Tracked execution of chore #1 on #2' => 'Esecuzione dell\'abitudine #1 registrata il #2',
'Tracked charge cycle of battery #1 on #2' => 'Ricarica della batteria #1 effettuata il #2',
'Consume all #1 which are currently in stock' => 'Consuma tutto #1 in dispensa',
'All' => 'Tutto',
'Track charge cycle of battery #1' => 'Registra la ricarica della batteria #1',
'Track execution of habit #1' => 'Registra l\'esecuzione dell\'abitudine #1',
'Track execution of chore #1' => 'Registra l\'esecuzione dell\'abitudine #1',
'Filter by location' => 'Filtra per posizione',
'Search' => 'Cerca',
'Not logged in' => 'Non autenticato',
'You have to select a product' => 'Devi selezionare un prodotto',
'You have to select a habit' => 'Devi selezionare un\'abitudine',
'You have to select a chore' => 'Devi selezionare un\'abitudine',
'You have to select a battery' => 'Devi selezionare una batteria',
'A name is required' => 'Inserisci un nome',
'A location is required' => 'Inserisci la posizione',

View File

@@ -2,27 +2,27 @@
return array(
'Stock overview' => 'Husholdning',
'#1 products expiring within the next #2 days' => '#1 Produkter som går ut på dato innen de neste #2 dagene',
'#1 products expiring within the next #2 days' => '#1 Produkt som går ut på dato innen de neste #2 dagene',
'#1 products are already expired' => '#1 Produkt som har gått ut på dato',
'#1 products are below defined min. stock amount' => '#1 Produkt under minimum husholdningsnivå',
'Product' => 'Produkt',
'Amount' => 'Antall',
'Next best before date' => 'Kommende best før dato',
'Logout' => 'Logg ut',
'Habits 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',
'Habit tracking' => 'Logge Husoppgaver',
'Chore tracking' => 'Logge Husarbeid',
'Battery tracking' => 'Batteri Ladesyklus',
'Products' => 'Produkter',
'Locations' => 'Lokasjoner',
'Quantity units' => 'Forpakning',
'Habits' => 'Husoppgaver',
'Chores' => 'Husarbeid',
'Batteries' => 'Batterier',
'Habit' => 'Husoppgave',
'Chore' => 'Husarbeid',
'Next estimated tracking' => 'Neste handling',
'Last tracked' => 'Sist logget',
'Battery' => 'Batteri',
@@ -30,7 +30,7 @@ return array(
'Next planned charge cycle' => 'Neste planlagte ladesyklus',
'Best before' => 'Best før',
'OK' => 'OK',
'Product overview' => 'Oversikt Produkt',
'Product overview' => 'Produkt oversikt',
'Stock quantity unit' => 'Forpakningstype i husholdningen',
'Stock amount' => 'Husholdning',
'Last purchased' => 'Sist kjøpt',
@@ -40,9 +40,9 @@ return array(
'will be added to the list of barcodes for the selected product on submit' => 'Blir lagt til liste over strekkoder når produkt blir lagt inn.',
'New amount' => 'Nytt antall',
'Note' => 'Info',
'Tracked time' => 'Tid logget',
'Habit overview' => 'Oversikt Husoppgave',
'Tracked count' => 'Logget',
'Tracked time' => 'Tid utført/ ladet',
'Chore overview' => 'Oversikt Husarbeid',
'Tracked count' => 'Antall utførelser/ ladninger',
'Battery overview' => 'Batteri Oversikt',
'Charge cycles count' => 'Antall ladesykluser',
'Create shopping list item' => 'Opprett handelisteoppføring',
@@ -67,12 +67,12 @@ return array(
'Create location' => 'Opprett lokasjon',
'Create quantity unit' => 'Opprett forpakning',
'Period type' => 'Gjentakelse',
'Period days' => 'Dager for gjentakelse',
'Create habit' => 'Opprett husoppgave',
'Period days' => 'Antall dager for gjentakelse',
'Create chore' => 'Opprett husarbeid oppgave',
'Used in' => 'Brukt',
'Create battery' => 'Opprett batteri',
'Edit battery' => 'Endre batteri',
'Edit habit' => 'Endre husoppgave',
'Edit chore' => 'Endre husarbeid oppgave',
'Edit quantity unit' => 'Endre forpakning',
'Edit product' => 'Endre produkt',
'Edit location' => 'Endre lokasjon',
@@ -80,7 +80,7 @@ return array(
'Manage master data' => 'Administrer masterdata',
'This will apply to added products' => 'Dette vil gjelde for produkt som blir lagt til',
'never' => 'aldri',
'Add products that are below defined min. stock amount' => 'Legg til produkt som er under definert minimums antall for husholdningen',
'Add products that are below defined min. stock amount' => 'Legg til produkt som er under minimumsnivå for husholdningen',
'For purchases this amount of days will be added to today for the best before date suggestion' => 'For innkjøp vil dette antallet dager legges til bestfør forslaget',
'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Dette betyr at 1 #1 innkjøp vil bli omgjort til #2 #3 husholdning',
'Login' => 'Logg inn',
@@ -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 habit "#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,31 +110,31 @@ 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 habit 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',
'Removed #1 #2 of #3 from stock' => 'Fjernet #1 #2 av #3 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 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 habits are due to be done within the next #2 days' => '#1 husoppgaver skal gjøres inne de #2 neste dagene',
'#1 habits 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 habit #1 on #2' => 'Logget utførelse av husoppgave "#1" den #2',
'Tracked charge cylce of battery #1 on #2' => 'Logget ladesyklus for batteri #1 og #2',
'Consume all #1 which are currently in stock' => 'Konsumér alle #1 som er i husholdningen',
'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' => 'Forbruk alle #1 som er i husholdningen',
'All' => 'Alle',
'Track charge cycle of battery #1' => 'Logg ladesyklus for batteri #1',
'Track execution of habit #1' => 'Logg utførelse av husoppgave #1',
'Track charge cycle of battery #1' => '#1 ladet',
'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 habit' => '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' => 'Et navn kreves',
'A name is required' => 'Vennligst fyll inn et navn',
'A location is required' => 'En lokasjon kreves',
'The amount cannot be lower than #1' => 'Antallet kan ikke være lavere enn #1',
'This cannot be negative' => 'Dette kan ikke være negativt',
@@ -154,20 +154,20 @@ return array(
'Are you sure to delete recipe ingredient "#1"?' => 'Er du sikker du ønsker å slette ingrediens "#1" fra oppskriften?',
'Are you sure to empty the shopping list?' => 'Er du sikker du ønsker å slette handlelisten?',
'Clear list' => 'Tøm liste',
'Requirements fulfilled' => 'Krav oppfylt',
'Requirements fulfilled' => 'Har jeg alt jeg trenger for denne oppskriften?',
'Put missing products on shopping list' => 'Legg manglende produkter til handlelisten',
'Not enough in stock, #1 ingredients missing' => 'Ikke nok i husholdningen, #1 ingredienser mangler',
'Enough in stock' => 'Nok i husholdningen',
'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Ikke nok i husholdningen, #1 ingrediens mangler, men står allerede på handelisten',
'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Ikke nok i husholdningen, #1 ingrediens mangler, men denne er på handelisten',
'Expand to fullscreen' => 'Full skjerm',
'Ingredients' => 'Ingredienser',
'Preparation' => 'Forberedelse / Slik gjør du',
'Recipe' => 'Oppskrift',
'Not enough in stock, #1 missing, #2 already on shopping list' => 'Ikke nok i husholdningen, #1 mangler, #2 allerede i handlisten',
'Not enough in stock, #1 missing, #2 already on shopping list' => 'Ikke nok i husholdningen, mangler #1, er #2 på handlelisten',
'Show notes' => 'Vis notater',
'Put missing amount on shopping list' => 'Legg manglende til handlelisten',
'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Er du sikker du ønsker å legge alle manglende ingredienser til oppskrift "#1"?',
'Added for recipe #1' => 'Lagt til oppskrift #1',
'Added for recipe #1' => 'Lagt til fra oppskrift "#1"',
'Manage users' => 'Administrer brukere',
'User' => 'Bruker',
'Users' => 'Brukere',
@@ -183,8 +183,8 @@ return array(
'Done by' => 'Utført av',
'Last done by' => 'Sist utført av',
'Unknown' => 'Ukjent',
'Filter by habit' => 'Filtrér husoppave',
'Habits 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 habit is due to be done within the next #2 days' => '#1 husoppgave skal gjøres inne de #2 neste dagene',
'#1 habit 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',
@@ -207,11 +207,52 @@ return array(
'in plural form' => 'I flertall',
'Never expires' => 'Går ikke ut på dato',
'This cannot be lower than #1' => 'Dette kan ikke være lavere enn #1',
'-1 means that this product never expires' => '-1 Betyr at dette produktet aldru går ut på dato',
'-1 means that this product never expires' => '-1 Betyr at dette produktet aldri går ut på dato',
'Quantity unit' => 'Forpakning',
'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' => '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',
//Constants
'manually' => 'Manuel',
'dynamic-regular' => 'Automatisk (rullering settes under)',
'dynamic-regular' => 'Automatisk',
//Technical component translations
'timeago_locale' => 'no',
@@ -233,9 +274,9 @@ return array(
'Glass' => 'Glass',
'Glasses' => 'Glass',
'Tin' => 'Hermetikkboks',
'Tin' => 'Hermetikkbokser',
'Tins' => 'Hermetikkbokser',
'Can' => 'Boks',
'Cans' => 'Boker',
'Cans' => 'Bokser',
'Bunch' => 'Klase',
'Bunches' => 'Klaser',
'Gummy bears' => 'Vingummibjørner',
@@ -257,7 +298,7 @@ return array(
'TV remote control' => 'Fjernkontroll for TV',
'Alarm clock' => 'Alarmklokke',
'Heat remote control' => 'Fjernkontroll for termostat',
'Lawn mowed in the garden' => 'Kuttet gresse i hagen',
'Lawn mowed in the garden' => 'Kuttet gresset i hagen',
'Some good snacks' => 'Noen gode snacks',
'Pizza dough' => 'Pizzadeig',
'Sieved tomatoes' => 'Tomatpuré',
@@ -272,5 +313,22 @@ return array(
'Italian' => 'Italiensk',
'Demo in different language' => 'Demo i annet språk',
'This is the note content of the recipe ingredient' => 'Dette er notisen for ingrediensen i oppskriften',
'Demo User' => 'Demo Bruker'
'Demo User' => 'Demo Bruker',
'Gram' => 'Gram',
'Grams' => 'Gram',
'Flour' => 'Mel',
'Pancakes' => 'Pannekaker',
'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'
);

View File

@@ -1,5 +1,5 @@
ALTER TABLE habits_log
ADD done_by_user_id;
ADD done_by_user_id INTEGER;
DROP TABLE api_keys;

31
migrations/0035.sql Normal file
View File

@@ -0,0 +1,31 @@
ALTER TABLE habits RENAME TO chores;
CREATE TABLE chores_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
chore_id INTEGER NOT NULL,
tracked_time DATETIME,
done_by_user_id INTEGER,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO chores_log
(chore_id, tracked_time, done_by_user_id, row_created_timestamp)
SELECT habit_id, tracked_time, done_by_user_id, row_created_timestamp
FROM habits_log;
DROP TABLE habits_log;
DROP VIEW habits_current;
CREATE VIEW chores_current
AS
SELECT
h.id AS chore_id,
MAX(l.tracked_time) AS last_tracked_time,
CASE h.period_type
WHEN 'manually' THEN '2999-12-31 23:59:59'
WHEN 'dynamic-regular' THEN datetime(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day')
END AS next_estimated_execution_time
FROM chores h
LEFT JOIN chores_log l
ON h.id = l.chore_id
GROUP BY h.id, h.period_days;

24
migrations/0036.sql Normal file
View File

@@ -0,0 +1,24 @@
CREATE TABLE tasks (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
due_date DATETIME,
done TINYINT NOT NULL DEFAULT 0 CHECK(done IN (0, 1)),
done_timestamp DATETIME,
category_id INTEGER,
assigned_to_user_id INTEGER,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE task_categories (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
CREATE VIEW tasks_current
AS
SELECT *
FROM tasks
WHERE done = 0;

9
migrations/0037.sql Normal file
View File

@@ -0,0 +1,9 @@
ALTER TABLE products
ADD product_group_id INTEGER;
CREATE TABLE product_groups (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);

27
migrations/0038.sql Normal file
View File

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

10
migrations/0039.sql Normal file
View File

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

View File

@@ -2,7 +2,7 @@
"name": "grocy",
"private": true,
"dependencies": {
"@danielfarrell/bootstrap-combobox": "https://github.com/pallidus-fintech/bootstrap-combobox.git#enhance/boostrap_4",
"@danielfarrell/bootstrap-combobox": "https://github.com/berrnd/bootstrap-combobox.git#master",
"@fortawesome/fontawesome-free": "^5.1.0",
"TagManager": "https://github.com/max-favilli/tagmanager.git#3.0.2",
"bootbox": "https://github.com/makeusabrew/bootbox.git#v5.x",
@@ -14,6 +14,8 @@
"datatables.net-colreorder-bs4": "^1.5.1",
"datatables.net-responsive": "^2.2.3",
"datatables.net-responsive-bs4": "^2.2.3",
"datatables.net-rowgroup": "^1.0.4",
"datatables.net-rowgroup-bs4": "^1.0.4",
"datatables.net-select": "^1.2.7",
"datatables.net-select-bs4": "^1.2.7",
"jquery": "^3.3.1",

View File

@@ -11,10 +11,6 @@ body {
white-space: normal;
}
.no-real-button {
pointer-events: none;
}
.timeago-contextual {
font-style: italic;
font-size: 0.8em;
@@ -54,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,
@@ -67,6 +64,23 @@ a.discrete-link:focus {
color: inherit;
}
.text-strike-through {
text-decoration: line-through;
}
button.disabled {
pointer-events: none;
}
/* Hide the default up/down arrow buttons for number inputs because we use our own buttons in numberpicker */
input[type='number'] {
-moz-appearance: textfield;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
/* Navigation style customizations */
#mainNav {
background-color: #e5e5e5 !important;
@@ -183,3 +197,8 @@ td {
.typeahead .active {
background-color: #e5e5e5;
}
/* Third party component customizations - Popper.js */
.tooltip {
pointer-events: none;
}

View File

@@ -0,0 +1,199 @@
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;
}

View File

@@ -31,3 +31,26 @@ GetUriParam = function(key)
}
}
};
IsTouchInputDevice = function()
{
if (("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch)
{
return true;
}
return false;
}
BoolVal = function(test)
{
var anything = test.toString().toLowerCase();
if (anything === true || anything === "true" || anything === "1" || anything === "on")
{
return true;
}
else
{
return false;
}
}

View File

@@ -3,6 +3,22 @@
var localizedText = Grocy.LocalizationStrings[text];
if (localizedText === undefined)
{
if (Grocy.Mode === 'dev')
{
jsonData = {};
jsonData.text = text;
Grocy.Api.Post('system/log-missing-localization', jsonData,
function(result)
{
// Nothing to do...
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
localizedText = text;
}
@@ -91,6 +107,14 @@ window.FontAwesomeConfig = {
searchPseudoElements: true
}
// Don't show tooltips on touch input devices
if (IsTouchInputDevice())
{
var css = document.createElement("style");
css.innerHTML = ".tooltip { display: none; }";
document.body.appendChild(css);
}
Grocy.Api = { };
Grocy.Api.Get = function(apiFunction, success, error)
{
@@ -168,3 +192,59 @@ Grocy.FrontendHelpers.ValidateForm = function(formId)
$(form).addClass('was-validated');
}
Grocy.FrontendHelpers.ShowGenericError = function(message, exception)
{
toastr.error(L(message) + '<br><br>' + L('Click to show technical details'), '', {
onclick: function()
{
bootbox.alert({
title: L('Error details'),
message: JSON.stringify(exception, null, 4)
});
}
});
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)
}
);
});

View File

@@ -0,0 +1,62 @@
Grocy.Api.Get('system/get-db-changed-time',
function(result)
{
Grocy.DatabaseChangedTime = moment(result.changed_time);
},
function(xhr)
{
console.error(xhr);
}
);
// 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,
// when there is no unsaved form data and when the user enabled auto reloading
setInterval(function()
{
Grocy.Api.Get('system/get-db-changed-time',
function(result)
{
var newDbChangedTime = moment(result.changed_time);
if (newDbChangedTime.isAfter(Grocy.DatabaseChangedTime))
{
if (Grocy.IdleTime >= 50)
{
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0)
{
window.location.reload();
}
}
Grocy.DatabaseChangedTime = newDbChangedTime;
}
},
function(xhr)
{
console.error(xhr);
}
);
}, 60000);
Grocy.IdleTime = 0;
Grocy.ResetIdleTime = function()
{
Grocy.IdleTime = 0;
}
window.onmousemove = Grocy.ResetIdleTime;
window.onmousedown = Grocy.ResetIdleTime;
window.onclick = Grocy.ResetIdleTime;
window.onscroll = Grocy.ResetIdleTime;
window.onkeypress = Grocy.ResetIdleTime;
// Increase the idle time once every second
// On any interaction it will be reset to 0 (see above)
setInterval(function()
{
Grocy.IdleTime += 1;
}, 1000);
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change))
{
$("#auto-reload-enabled").prop("checked", true);
}

View File

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

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()
@@ -21,8 +30,35 @@ $("#search").on("keyup", function()
batteriesOverviewTable.search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
// Transfer CSS classes of selected element to dropdown element (for background)
$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control");
batteriesOverviewTable.column(4).search(value).draw();
});
$(".status-filter-button").on("click", function()
{
var value = $(this).data("status-filter");
$("#status-filter").val(value);
$("#status-filter").trigger("change");
});
$(document).on('click', '.track-charge-cycle-button', function(e)
{
e.preventDefault();
// Remove the focus from the current button
// to prevent that the tooltip stays until clicked anywhere else
document.activeElement.blur();
var batteryId = $(e.currentTarget).attr('data-battery-id');
var batteryName = $(e.currentTarget).attr('data-battery-name');
var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss');
@@ -66,7 +102,7 @@ $(document).on('click', '.track-charge-cycle-button', function(e)
$('#battery-' + batteryId + '-next-charge-time-timeago').attr('datetime', result.next_estimated_charge_time);
}
toastr.success(L('Tracked charge cylce of battery #1 on #2', batteryName, trackedTime));
toastr.success(L('Tracked charge cycle of battery #1 on #2', batteryName, trackedTime));
RefreshContextualTimeago();
RefreshStatistics();
},

View File

@@ -11,7 +11,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -24,7 +24,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -39,9 +39,10 @@ $('#battery-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('battery-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -49,7 +49,8 @@ $('#battery_id').on('change', function(e)
});
$('.combobox').combobox({
appendId: '_text_input'
appendId: '_text_input',
bsVersion: '4'
});
$('#battery_id').val('');
@@ -67,9 +68,10 @@ $('#batterytracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('batterytracking-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -0,0 +1,72 @@
$('#save-chore-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/chores', $('#chore-form').serializeJSON(),
function(result)
{
window.location.href = U('/chores');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Post('edit-object/chores/' + Grocy.EditObjectId, $('#chore-form').serializeJSON(),
function(result)
{
window.location.href = U('/chores');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#chore-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('chore-form');
});
$('#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
{
return false;
}
else
{
$('#save-chore-button').click();
}
}
});
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('chore-form');
$('.input-group-chore-period-type').on('change', function(e)
{
var periodType = $('#period_type').val();
var periodDays = $('#period_days').val();
if (periodType === 'dynamic-regular')
{
$('#chore-period-type-info').text(L('This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked', periodDays.toString()));
$('#chore-period-type-info').removeClass('d-none');
}
else
{
$('#chore-period-type-info').addClass('d-none');
}
});

View File

@@ -1,4 +1,4 @@
var habitsTable = $('#habits-table').DataTable({
var choresTable = $('#chores-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
'columnDefs': [
@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()
@@ -18,16 +27,16 @@ $("#search").on("keyup", function()
value = "";
}
habitsTable.search(value).draw();
choresTable.search(value).draw();
});
$(document).on('click', '.habit-delete-button', function (e)
$(document).on('click', '.chore-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-habit-name');
var objectId = $(e.currentTarget).attr('data-habit-id');
var objectName = $(e.currentTarget).attr('data-chore-name');
var objectId = $(e.currentTarget).attr('data-chore-id');
bootbox.confirm({
message: L('Are you sure to delete habit "#1"?', objectName),
message: L('Are you sure to delete chore "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
@@ -42,10 +51,10 @@ $(document).on('click', '.habit-delete-button', function (e)
{
if (result === true)
{
Grocy.Api.Get('delete-object/habits/' + objectId,
Grocy.Api.Get('delete-object/chores/' + objectId,
function(result)
{
window.location.href = U('/habits');
window.location.href = U('/chores');
},
function(xhr)
{

View File

@@ -0,0 +1,46 @@
var choresAnalysisTable = $('#chores-analysis-table').DataTable({
'paginate': false,
'order': [[1, 'desc']],
'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 = "";
});
}
});
$("#chore-filter").on("change", function()
{
var value = $(this).val();
var text = $("#chore-filter option:selected").text();
if (value === "all")
{
text = "";
}
choresAnalysisTable.column(0).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
choresAnalysisTable.search(value).draw();
});
if (typeof GetUriParam("chore") !== "undefined")
{
$("#chore-filter").val(GetUriParam("chore"));
$("#chore-filter").trigger("change");
}

View File

@@ -0,0 +1,154 @@
var choresOverviewTable = $('#chores-overview-table').DataTable({
'paginate': false,
'order': [[2, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'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 = "";
});
}
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
choresOverviewTable.search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
// Transfer CSS classes of selected element to dropdown element (for background)
$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control");
choresOverviewTable.column(4).search(value).draw();
});
$(".status-filter-button").on("click", function()
{
var value = $(this).data("status-filter");
$("#status-filter").val(value);
$("#status-filter").trigger("change");
});
$(document).on('click', '.track-chore-button', function(e)
{
e.preventDefault();
// Remove the focus from the current button
// to prevent that the tooltip stays until clicked anywhere else
document.activeElement.blur();
var choreId = $(e.currentTarget).attr('data-chore-id');
var choreName = $(e.currentTarget).attr('data-chore-name');
var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss');
Grocy.Api.Get('chores/track-chore-execution/' + choreId + '?tracked_time=' + trackedTime,
function()
{
Grocy.Api.Get('chores/get-chore-details/' + choreId,
function(result)
{
var choreRow = $('#chore-' + choreId + '-row');
var nextXDaysThreshold = moment().add($("#info-due-chores").data("next-x-days"), "days");
var now = moment();
var nextExecutionTime = moment(result.next_estimated_execution_time);
choreRow.removeClass("table-warning");
choreRow.removeClass("table-danger");
if (nextExecutionTime.isBefore(now))
{
choreRow.addClass("table-danger");
}
else if (nextExecutionTime.isBefore(nextXDaysThreshold))
{
choreRow.addClass("table-warning");
}
$('#chore-' + choreId + '-last-tracked-time').parent().effect('highlight', { }, 500);
$('#chore-' + choreId + '-last-tracked-time').fadeOut(500, function()
{
$(this).text(trackedTime).fadeIn(500);
});
$('#chore-' + choreId + '-last-tracked-time-timeago').attr('datetime', trackedTime);
if (result.chore.period_type == "dynamic-regular")
{
$('#chore-' + choreId + '-next-execution-time').parent().effect('highlight', { }, 500);
$('#chore-' + choreId + '-next-execution-time').fadeOut(500, function()
{
$(this).text(result.next_estimated_execution_time).fadeIn(500);
});
$('#chore-' + choreId + '-next-execution-time-timeago').attr('datetime', result.next_estimated_execution_time);
}
toastr.success(L('Tracked execution of chore #1 on #2', choreName, trackedTime));
RefreshContextualTimeago();
RefreshStatistics();
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
function RefreshStatistics()
{
var nextXDays = $("#info-due-chores").data("next-x-days");
Grocy.Api.Get('chores/get-current',
function(result)
{
var dueCount = 0;
var overdueCount = 0;
var now = moment();
var nextXDaysThreshold = moment().add(nextXDays, "days");
result.forEach(element => {
var date = moment(element.next_estimated_execution_time);
if (date.isBefore(now))
{
overdueCount++;
}
else if (date.isBefore(nextXDaysThreshold))
{
dueCount++;
}
});
$("#info-due-chores").text(Pluralize(dueCount, L('#1 chore is due to be done within the next #2 days', dueCount, nextXDays), L('#1 chores are due to be done within the next #2 days', dueCount, nextXDays)));
$("#info-overdue-chores").text(Pluralize(overdueCount, L('#1 chore is overdue to be done', overdueCount), L('#1 chores are overdue to be done', overdueCount)));
},
function(xhr)
{
console.error(xhr);
}
);
}
RefreshStatistics();

View File

@@ -0,0 +1,84 @@
$('#save-choretracking-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#choretracking-form').serializeJSON();
Grocy.Api.Get('chores/get-chore-details/' + jsonForm.chore_id,
function (choreDetails)
{
Grocy.Api.Get('chores/track-chore-execution/' + jsonForm.chore_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(),
function(result)
{
toastr.success(L('Tracked execution of chore #1 on #2', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue()));
$('#chore_id').val('');
$('#chore_id_text_input').focus();
$('#chore_id_text_input').val('');
Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD HH:mm:ss'));
$('#chore_id_text_input').trigger('change');
Grocy.FrontendHelpers.ValidateForm('choretracking-form');
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#chore_id').on('change', function(e)
{
var input = $('#chore_id_text_input').val().toString();
$('#chore_id_text_input').val(input);
$('#chore_id').data('combobox').refresh();
var choreId = $(e.target).val();
if (choreId)
{
Grocy.Components.ChoreCard.Refresh(choreId);
Grocy.Components.DateTimePicker.GetInputElement().focus();
Grocy.FrontendHelpers.ValidateForm('choretracking-form');
}
});
$('.combobox').combobox({
appendId: '_text_input',
bsVersion: '4'
});
$('#chore_id_text_input').focus();
$('#chore_id_text_input').trigger('change');
Grocy.FrontendHelpers.ValidateForm('choretracking-form');
$('#choretracking-form input').keyup(function (event)
{
Grocy.FrontendHelpers.ValidateForm('choretracking-form');
});
$('#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
{
return false;
}
else
{
$('#save-choretracking-button').click();
}
}
});
Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
{
Grocy.FrontendHelpers.ValidateForm('choretracking-form');
});

View File

@@ -0,0 +1,21 @@
Grocy.Components.ChoreCard = { };
Grocy.Components.ChoreCard.Refresh = function(choreId)
{
Grocy.Api.Get('chores/get-chore-details/' + choreId,
function(choreDetails)
{
$('#chorecard-chore-name').text(choreDetails.chore.name);
$('#chorecard-chore-last-tracked').text((choreDetails.last_tracked || L('never')));
$('#chorecard-chore-last-tracked-timeago').text($.timeago(choreDetails.last_tracked || ''));
$('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0'));
$('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || L('Unknown')));
EmptyElementWhenMatches('#chorecard-chore-last-tracked-timeago', L('timeago_nan'));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@@ -14,6 +14,14 @@ Grocy.Components.DateTimePicker.SetValue = function(value)
{
Grocy.Components.DateTimePicker.GetInputElement().val(value);
Grocy.Components.DateTimePicker.GetInputElement().trigger('change');
// "Click" the shortcut checkbox when the desired value is
// not the shortcut value and it is currently set
var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value");
if (value != shortcutValue && $("#datetimepicker-shortcut").is(":checked"))
{
$("#datetimepicker-shortcut").click();
}
}
var startDate = null;
@@ -21,6 +29,10 @@ if (Grocy.Components.DateTimePicker.GetInputElement().data('init-with-now') ===
{
startDate = moment().format(Grocy.Components.DateTimePicker.GetInputElement().data('format'));
}
if (Grocy.Components.DateTimePicker.GetInputElement().data('init-value').length > 0)
{
startDate = moment(Grocy.Components.DateTimePicker.GetInputElement().data('init-value')).format(Grocy.Components.DateTimePicker.GetInputElement().data('format'));
}
var limitDate = moment('2999-12-31 23:59:59');
if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-end-to-now') === true)

View File

@@ -1,21 +0,0 @@
Grocy.Components.HabitCard = { };
Grocy.Components.HabitCard.Refresh = function(habitId)
{
Grocy.Api.Get('habits/get-habit-details/' + habitId,
function(habitDetails)
{
$('#habitcard-habit-name').text(habitDetails.habit.name);
$('#habitcard-habit-last-tracked').text((habitDetails.last_tracked || L('never')));
$('#habitcard-habit-last-tracked-timeago').text($.timeago(habitDetails.last_tracked || ''));
$('#habitcard-habit-tracked-count').text((habitDetails.tracked_count || '0'));
$('#habitcard-habit-last-done-by').text((habitDetails.last_done_by.display_name || L('Unknown')));
EmptyElementWhenMatches('#habitcard-habit-last-tracked-timeago', L('timeago_nan'));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@@ -0,0 +1,15 @@
$(".numberpicker-down-button").unbind('click').on("click", function ()
{
var inputElement = $(this).parent().parent().find('input[type="number"]')[0];
inputElement.stepDown();
$(inputElement).trigger('keyup');
$(inputElement).trigger('change');
});
$(".numberpicker-up-button").unbind('click').on("click", function()
{
var inputElement = $(this).parent().parent().find('input[type="number"]')[0];
inputElement.stepUp();
$(inputElement).trigger('keyup');
$(inputElement).trigger('change');
});

View File

@@ -45,7 +45,8 @@ Grocy.Components.ProductPicker.HideCustomError = function()
$('.product-combobox').combobox({
appendId: '_text_input',
bsVersion: '4'
bsVersion: '4',
clearIfNoMatch: false
});
var prefillProduct = GetUriParam('createdproduct');
@@ -81,8 +82,13 @@ if (addBarcode !== undefined)
$('#barcode-lookup-disabled-hint').removeClass('d-none');
}
$('#product_id_text_input').on('change', function(e)
$('#product_id_text_input').on('blur', function(e)
{
if (Grocy.Components.ProductPicker.GetPicker().hasClass("combobox-menu-visible"))
{
return;
}
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
@@ -106,14 +112,20 @@ $('#product_id_text_input').on('change', function(e)
bootbox.dialog({
message: L('"#1" could not be resolved to a product, how do you want to proceed?', input),
title: L('Create or assign product'),
onEscape: function() { },
onEscape: function()
{
Grocy.Components.ProductPicker.SetValue('');
},
size: 'large',
backdrop: true,
buttons: {
cancel: {
label: 'Cancel',
label: L('Cancel'),
className: 'btn-default responsive-button',
callback: function() { }
callback: function()
{
Grocy.Components.ProductPicker.SetValue('');
}
},
addnewproduct: {
label: '<strong>P</strong> ' + L('Add as new product'),

View File

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

View File

@@ -1,71 +0,0 @@
$('#save-habit-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/habits', $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = U('/habits');
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
Grocy.Api.Post('edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = U('/habits');
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$('#habit-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('habit-form');
});
$('#habit-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if (document.getElementById('habit-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else
{
$('#save-habit-button').click();
}
}
});
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('habit-form');
$('.input-group-habit-period-type').on('change', function(e)
{
var periodType = $('#period_type').val();
var periodDays = $('#period_days').val();
if (periodType === 'dynamic-regular')
{
$('#habit-period-type-info').text(L('This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked', periodDays.toString()));
$('#habit-period-type-info').removeClass('d-none');
}
else
{
$('#habit-period-type-info').addClass('d-none');
}
});

View File

@@ -1,37 +0,0 @@
var habitsAnalysisTable = $('#habits-analysis-table').DataTable({
'paginate': false,
'order': [[1, 'desc']],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
});
$("#habit-filter").on("change", function()
{
var value = $(this).val();
var text = $("#habit-filter option:selected").text();
if (value === "all")
{
text = "";
}
habitsAnalysisTable.column(0).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
habitsAnalysisTable.search(value).draw();
});
if (typeof GetUriParam("habit") !== "undefined")
{
$("#habit-filter").val(GetUriParam("habit"));
$("#habit-filter").trigger("change");
}

View File

@@ -1,118 +0,0 @@
var habitsOverviewTable = $('#habits-overview-table').DataTable({
'paginate': false,
'order': [[2, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
habitsOverviewTable.search(value).draw();
});
$(document).on('click', '.track-habit-button', function(e)
{
var habitId = $(e.currentTarget).attr('data-habit-id');
var habitName = $(e.currentTarget).attr('data-habit-name');
var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss');
Grocy.Api.Get('habits/track-habit-execution/' + habitId + '?tracked_time=' + trackedTime,
function()
{
Grocy.Api.Get('habits/get-habit-details/' + habitId,
function(result)
{
var habitRow = $('#habit-' + habitId + '-row');
var nextXDaysThreshold = moment().add($("#info-due-habits").data("next-x-days"), "days");
var now = moment();
var nextExecutionTime = moment(result.next_estimated_execution_time);
habitRow.removeClass("table-warning");
habitRow.removeClass("table-danger");
if (nextExecutionTime.isBefore(now))
{
habitRow.addClass("table-danger");
}
else if (nextExecutionTime.isBefore(nextXDaysThreshold))
{
habitRow.addClass("table-warning");
}
$('#habit-' + habitId + '-last-tracked-time').parent().effect('highlight', { }, 500);
$('#habit-' + habitId + '-last-tracked-time').fadeOut(500, function()
{
$(this).text(trackedTime).fadeIn(500);
});
$('#habit-' + habitId + '-last-tracked-time-timeago').attr('datetime', trackedTime);
if (result.habit.period_type == "dynamic-regular")
{
$('#habit-' + habitId + '-next-execution-time').parent().effect('highlight', { }, 500);
$('#habit-' + habitId + '-next-execution-time').fadeOut(500, function()
{
$(this).text(result.next_estimated_execution_time).fadeIn(500);
});
$('#habit-' + habitId + '-next-execution-time-timeago').attr('datetime', result.next_estimated_execution_time);
}
toastr.success(L('Tracked execution of habit #1 on #2', habitName, trackedTime));
RefreshContextualTimeago();
RefreshStatistics();
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
function RefreshStatistics()
{
var nextXDays = $("#info-due-habits").data("next-x-days");
Grocy.Api.Get('habits/get-current',
function(result)
{
var dueCount = 0;
var overdueCount = 0;
var now = moment();
var nextXDaysThreshold = moment().add(nextXDays, "days");
result.forEach(element => {
var date = moment(element.next_estimated_execution_time);
if (date.isBefore(now))
{
overdueCount++;
}
else if (date.isBefore(nextXDaysThreshold))
{
dueCount++;
}
});
$("#info-due-habits").text(Pluralize(dueCount, L('#1 habit is due to be done within the next #2 days', dueCount, nextXDays), L('#1 habits are due to be done within the next #2 days', dueCount, nextXDays)));
$("#info-overdue-habits").text(Pluralize(overdueCount, L('#1 habit is overdue to be done', overdueCount), L('#1 habits are overdue to be done', overdueCount)));
},
function(xhr)
{
console.error(xhr);
}
);
}
RefreshStatistics();

View File

@@ -1,82 +0,0 @@
$('#save-habittracking-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#habittracking-form').serializeJSON();
Grocy.Api.Get('habits/get-habit-details/' + jsonForm.habit_id,
function (habitDetails)
{
Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(),
function(result)
{
toastr.success(L('Tracked execution of habit #1 on #2', habitDetails.habit.name, Grocy.Components.DateTimePicker.GetValue()));
$('#habit_id').val('');
$('#habit_id_text_input').focus();
$('#habit_id_text_input').val('');
Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD HH:mm:ss'));
$('#habit_id_text_input').trigger('change');
Grocy.FrontendHelpers.ValidateForm('habittracking-form');
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#habit_id').on('change', function(e)
{
var input = $('#habit_id_text_input').val().toString();
$('#habit_id_text_input').val(input);
$('#habit_id').data('combobox').refresh();
var habitId = $(e.target).val();
if (habitId)
{
Grocy.Components.HabitCard.Refresh(habitId);
Grocy.Components.DateTimePicker.GetInputElement().focus();
Grocy.FrontendHelpers.ValidateForm('habittracking-form');
}
});
$('.combobox').combobox({
appendId: '_text_input'
});
$('#habit_id_text_input').focus();
$('#habit_id_text_input').trigger('change');
Grocy.FrontendHelpers.ValidateForm('habittracking-form');
$('#habittracking-form input').keyup(function (event)
{
Grocy.FrontendHelpers.ValidateForm('habittracking-form');
});
$('#habittracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if (document.getElementById('habittracking-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else
{
$('#save-habittracking-button').click();
}
}
});
Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
{
Grocy.FrontendHelpers.ValidateForm('habittracking-form');
});

View File

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

View File

@@ -11,7 +11,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -24,7 +24,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -39,9 +39,10 @@ $('#location-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('location-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
var createdApiKeyId = GetUriParam('CreatedApiKeyId');

View File

@@ -18,7 +18,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -31,7 +31,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -99,9 +99,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

View File

@@ -0,0 +1,56 @@
$('#save-product-group-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/product_groups', $('#product-group-form').serializeJSON(),
function(result)
{
window.location.href = U('/productgroups');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Post('edit-object/product_groups/' + Grocy.EditObjectId, $('#product-group-form').serializeJSON(),
function(result)
{
window.location.href = U('/productgroups');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#product-group-form input').keyup(function (event)
{
Grocy.FrontendHelpers.ValidateForm('product-group-form');
});
$('#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
{
return false;
}
else
{
$('#save-product-group-button').click();
}
}
});
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('product-group-form');

View File

@@ -0,0 +1,67 @@
var groupsTable = $('#productgroups-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'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 = "";
});
}
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
groupsTable.search(value).draw();
});
$(document).on('click', '.product-group-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-group-name');
var objectId = $(e.currentTarget).attr('data-group-id');
bootbox.confirm({
message: L('Are you sure to delete product group "#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/product_groups/' + objectId,
function(result)
{
window.location.href = U('/productgroups');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()

View File

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

View File

@@ -11,7 +11,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -24,7 +24,7 @@
},
function(xhr)
{
console.error(xhr);
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
@@ -39,9 +39,10 @@ $('#quantityunit-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('quantityunit-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()

View File

@@ -23,7 +23,16 @@ var recipesPosTables = $('#recipes-pos-table').DataTable({
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function ()
@@ -49,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

View File

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

View File

@@ -5,6 +5,15 @@
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
},
'select': 'single',
'initComplete': function()
{
@@ -149,4 +158,5 @@ recipesTables.on('select', function(e, dt, type, indexes)
$("#selectedRecipeToggleFullscreenButton").on('click', function(e)
{
$("#selectedRecipeCard").toggleClass("fullscreen");
$("#selectedRecipeCard .card-header").toggleClass("fixed-top");
});

View File

@@ -1,13 +1,27 @@
var shoppingListTable = $('#shoppinglist-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
"orderFixed": [[3, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
{ 'orderable': false, 'targets': 0 },
{ 'visible': false, 'targets': 3 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
},
'rowGroup': {
dataSrc: 3
}
});
$("#search").on("keyup", function()
@@ -21,8 +35,31 @@ $("#search").on("keyup", function()
shoppingListTable.search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
// Transfer CSS classes of selected element to dropdown element (for background)
$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control");
shoppingListTable.column(4).search(value).draw();
});
$(".status-filter-button").on("click", function()
{
var value = $(this).data("status-filter");
$("#status-filter").val(value);
$("#status-filter").trigger("change");
});
$(document).on('click', '.shoppinglist-delete-button', function (e)
{
e.preventDefault();
var shoppingListItemId = $(e.currentTarget).attr('data-shoppinglist-id');
Grocy.Api.Get('delete-object/shopping_list/' + shoppingListItemId,

View File

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

View File

@@ -8,7 +8,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#location-filter").on("change", function()
@@ -22,6 +31,27 @@ $("#location-filter").on("change", function()
stockOverviewTable.column(4).search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
// Transfer CSS classes of selected element to dropdown element (for background)
$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control");
stockOverviewTable.column(5).search(value).draw();
});
$(".status-filter-button").on("click", function()
{
var value = $(this).data("status-filter");
$("#status-filter").val(value);
$("#status-filter").trigger("change");
});
$("#search").on("keyup", function()
{
var value = $(this).val();
@@ -35,6 +65,12 @@ $("#search").on("keyup", function()
$(document).on('click', '.product-consume-button', function(e)
{
e.preventDefault();
// Remove the focus from the current button
// to prevent that the tooltip stays until clicked anywhere else
document.activeElement.blur();
var productId = $(e.currentTarget).attr('data-product-id');
var productName = $(e.currentTarget).attr('data-product-name');
var productQuName = $(e.currentTarget).attr('data-product-qu-name');

View File

@@ -0,0 +1,67 @@
var categoriesTable = $('#taskcategories-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'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 = "";
});
}
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
categoriesTable.search(value).draw();
});
$(document).on('click', '.task-category-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-category-name');
var objectId = $(e.currentTarget).attr('data-category-id');
bootbox.confirm({
message: L('Are you sure to delete task category "#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/task_categories/' + objectId,
function(result)
{
window.location.href = U('/taskcategories');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});

View File

@@ -0,0 +1,56 @@
$('#save-task-category-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/task_categories', $('#task-category-form').serializeJSON(),
function(result)
{
window.location.href = U('/taskcategories');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Post('edit-object/task_categories/' + Grocy.EditObjectId, $('#task-category-form').serializeJSON(),
function(result)
{
window.location.href = U('/taskcategories');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#task-category-form input').keyup(function (event)
{
Grocy.FrontendHelpers.ValidateForm('task-category-form');
});
$('#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
{
return false;
}
else
{
$('#save-task-category-button').click();
}
}
});
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('task-category-form');

61
public/viewjs/taskform.js Normal file
View File

@@ -0,0 +1,61 @@
$('#save-task-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#task-form').serializeJSON();
jsonData.assigned_to_user_id = jsonData.user_id;
delete jsonData.user_id;
jsonData.due_date = Grocy.Components.DateTimePicker.GetValue();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/tasks', jsonData,
function(result)
{
window.location.href = U('/tasks');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Post('edit-object/tasks/' + Grocy.EditObjectId, jsonData,
function(result)
{
window.location.href = U('/tasks');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#task-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('task-form');
});
$('#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
{
return false;
}
else
{
$('#save-task-button').click();
}
}
});
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('task-form');

188
public/viewjs/tasks.js Normal file
View File

@@ -0,0 +1,188 @@
var tasksTable = $('#tasks-table').DataTable({
'paginate': false,
'order': [[2, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'visible': false, 'targets': 3 }
],
'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 = "";
});
},
'rowGroup': {
dataSrc: 3
}
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
tasksTable.search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
// Transfer CSS classes of selected element to dropdown element (for background)
$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control");
tasksTable.column(5).search(value).draw();
});
$(".status-filter-button").on("click", function()
{
var value = $(this).data("status-filter");
$("#status-filter").val(value);
$("#status-filter").trigger("change");
});
$(document).on('click', '.do-task-button', function(e)
{
e.preventDefault();
// Remove the focus from the current button
// to prevent that the tooltip stays until clicked anywhere else
document.activeElement.blur();
var taskId = $(e.currentTarget).attr('data-task-id');
var taskName = $(e.currentTarget).attr('data-task-name');
var doneTime = moment().format('YYYY-MM-DD HH:mm:ss');
Grocy.Api.Get('tasks/mark-task-as-completed/' + taskId + '?done_time=' + doneTime,
function()
{
if (!$("#show-done-tasks").is(":checked"))
{
$('#task-' + taskId + '-row').fadeOut(500, function ()
{
$(this).remove();
});
}
else
{
$('#task-' + taskId + '-row').addClass("text-muted");
$('#task-' + taskId + '-name').addClass("text-strike-through");
$('.do-task-button[data-task-id="' + taskId + '"]').addClass("disabled");
}
toastr.success(L('Marked task #1 as completed on #2', taskName, doneTime));
RefreshContextualTimeago();
RefreshStatistics();
},
function(xhr)
{
console.error(xhr);
}
);
});
$(document).on('click', '.delete-task-button', function (e)
{
e.preventDefault();
var objectName = $(e.currentTarget).attr('data-task-name');
var objectId = $(e.currentTarget).attr('data-task-id');
bootbox.confirm({
message: L('Are you sure to delete task "#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/tasks/' + objectId,
function(result)
{
$('#task-' + objectId + '-row').fadeOut(500, function ()
{
$(this).remove();
});
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$("#show-done-tasks").change(function()
{
if (this.checked)
{
window.location.href = U('/tasks?include_done');
}
else
{
window.location.href = U('/tasks');
}
});
if (GetUriParam('include_done'))
{
$("#show-done-tasks").prop('checked', true);
}
function RefreshStatistics()
{
var nextXDays = $("#info-due-tasks").data("next-x-days");
Grocy.Api.Get('tasks/get-current',
function(result)
{
var dueCount = 0;
var overdueCount = 0;
var now = moment();
var nextXDaysThreshold = moment().add(nextXDays, "days");
result.forEach(element => {
var date = moment(element.due_date);
if (date.isBefore(now))
{
overdueCount++;
}
else if (date.isBefore(nextXDaysThreshold))
{
dueCount++;
}
});
$("#info-due-tasks").text(Pluralize(dueCount, L('#1 task is due to be done within the next #2 days', dueCount, nextXDays), L('#1 tasks are due to be done within the next #2 days', dueCount, nextXDays)));
$("#info-overdue-tasks").text(Pluralize(overdueCount, L('#1 task is overdue to be done', overdueCount), L('#1 tasks are overdue to be done', overdueCount)));
},
function(xhr)
{
console.error(xhr);
}
);
}
RefreshStatistics();

View File

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

View File

@@ -7,7 +7,16 @@
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
}
});
$("#search").on("keyup", function()

View File

@@ -30,6 +30,8 @@ $app->group('', function()
$this->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm');
$this->get('/quantityunits', '\Grocy\Controllers\StockController:QuantityUnitsList');
$this->get('/quantityunit/{quantityunitId}', '\Grocy\Controllers\StockController:QuantityUnitEditForm');
$this->get('/productgroups', '\Grocy\Controllers\StockController:ProductGroupsList');
$this->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm');
$this->get('/shoppinglist', '\Grocy\Controllers\StockController:ShoppingList');
$this->get('/shoppinglistitem/{itemId}', '\Grocy\Controllers\StockController:ShoppingListItemEditForm');
@@ -38,13 +40,13 @@ $app->group('', function()
$this->get('/recipe/{recipeId}', '\Grocy\Controllers\RecipesController:RecipeEditForm');
$this->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm');
// Habit routes
$this->get('/habitsoverview', '\Grocy\Controllers\HabitsController:Overview');
$this->get('/habittracking', '\Grocy\Controllers\HabitsController:TrackHabitExecution');
$this->get('/habitsanalysis', '\Grocy\Controllers\HabitsController:Analysis');
// Chore routes
$this->get('/choresoverview', '\Grocy\Controllers\ChoresController:Overview');
$this->get('/choretracking', '\Grocy\Controllers\ChoresController:TrackChoreExecution');
$this->get('/choresanalysis', '\Grocy\Controllers\ChoresController:Analysis');
$this->get('/habits', '\Grocy\Controllers\HabitsController:HabitsList');
$this->get('/habit/{habitId}', '\Grocy\Controllers\HabitsController:HabitEditForm');
$this->get('/chores', '\Grocy\Controllers\ChoresController:ChoresList');
$this->get('/chore/{choreId}', '\Grocy\Controllers\ChoresController:ChoreEditForm');
// Battery routes
$this->get('/batteriesoverview', '\Grocy\Controllers\BatteriesController:Overview');
@@ -53,6 +55,12 @@ $app->group('', function()
$this->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList');
$this->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm');
// Task routes
$this->get('/tasks', '\Grocy\Controllers\TasksController:Overview');
$this->get('/task/{taskId}', '\Grocy\Controllers\TasksController:TaskEditForm');
$this->get('/taskcategories', '\Grocy\Controllers\TasksController:TaskCategoriesList');
$this->get('/taskcategory/{categoryId}', '\Grocy\Controllers\TasksController:TaskCategoryEditForm');
// OpenAPI routes
$this->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi');
$this->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList');
@@ -71,12 +79,23 @@ $app->group('/api', function()
$this->post('/edit-object/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:EditObject');
$this->get('/delete-object/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:DeleteObject');
// 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');
// Users
$this->get('/users/get', '\Grocy\Controllers\UsersApiController:GetUsers');
$this->post('/users/create', '\Grocy\Controllers\UsersApiController:CreateUser');
$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');
@@ -93,15 +112,19 @@ $app->group('/api', function()
$this->get('/recipes/add-not-fulfilled-products-to-shopping-list/{recipeId}', '\Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList');
$this->get('/recipes/consume-recipe/{recipeId}', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe');
// Habits
$this->get('/habits/track-habit-execution/{habitId}', '\Grocy\Controllers\HabitsApiController:TrackHabitExecution');
$this->get('/habits/get-habit-details/{habitId}', '\Grocy\Controllers\HabitsApiController:HabitDetails');
$this->get('/habits/get-current', '\Grocy\Controllers\HabitsApiController:Current');
// Chores
$this->get('/chores/track-chore-execution/{choreId}', '\Grocy\Controllers\ChoresApiController:TrackChoreExecution');
$this->get('/chores/get-chore-details/{choreId}', '\Grocy\Controllers\ChoresApiController:ChoreDetails');
$this->get('/chores/get-current', '\Grocy\Controllers\ChoresApiController:Current');
// Batteries
$this->get('/batteries/track-charge-cycle/{batteryId}', '\Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$this->get('/batteries/get-battery-details/{batteryId}', '\Grocy\Controllers\BatteriesApiController:BatteryDetails');
$this->get('/batteries/get-current', '\Grocy\Controllers\BatteriesApiController:Current');
// Tasks
$this->get('/tasks/get-current', '\Grocy\Controllers\TasksApiController:Current');
$this->get('/tasks/mark-task-as-completed/{taskId}', '\Grocy\Controllers\TasksApiController:MarkTaskAsCompleted');
})->add(new ApiKeyAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName(), $appContainer->ApiKeyHeaderName))
->add(JsonMiddleware::class)
->add(new CorsMiddleware([

View File

@@ -0,0 +1,74 @@
<?php
namespace Grocy\Services;
class ChoresService extends BaseService
{
const CHORE_TYPE_MANUALLY = 'manually';
const CHORE_TYPE_DYNAMIC_REGULAR = 'dynamic-regular';
public function GetCurrent()
{
$sql = 'SELECT * from chores_current';
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetChoreDetails(int $choreId)
{
if (!$this->ChoreExists($choreId))
{
throw new \Exception('Chore does not exist');
}
$chore = $this->Database->chores($choreId);
$choreTrackedCount = $this->Database->chores_log()->where('chore_id', $choreId)->count();
$choreLastTrackedTime = $this->Database->chores_log()->where('chore_id', $choreId)->max('tracked_time');
$nextExeuctionTime = $this->Database->chores_current()->where('chore_id', $choreId)->min('next_estimated_execution_time');
$lastChoreLogRow = $this->Database->chores_log()->where('chore_id = :1 AND tracked_time = :2', $choreId, $choreLastTrackedTime)->fetch();
$lastDoneByUser = null;
if ($lastChoreLogRow !== null && !empty($lastChoreLogRow))
{
$usersService = new UsersService();
$users = $usersService->GetUsersAsDto();
$lastDoneByUser = FindObjectInArrayByPropertyValue($users, 'id', $lastChoreLogRow->done_by_user_id);
}
return array(
'chore' => $chore,
'last_tracked' => $choreLastTrackedTime,
'tracked_count' => $choreTrackedCount,
'last_done_by' => $lastDoneByUser,
'next_estimated_execution_time' => $nextExeuctionTime
);
}
public function TrackChore(int $choreId, string $trackedTime, $doneBy = GROCY_USER_ID)
{
if (!$this->ChoreExists($choreId))
{
throw new \Exception('Chore does not exist');
}
$userRow = $this->Database->users()->where('id = :1', $doneBy)->fetch();
if ($userRow === null)
{
throw new \Exception('User does not exist');
}
$logRow = $this->Database->chores_log()->createRow(array(
'chore_id' => $choreId,
'tracked_time' => $trackedTime,
'done_by_user_id' => $doneBy
));
$logRow->save();
return true;
}
private function ChoreExists($choreId)
{
$choreRow = $this->Database->chores()->where('id = :1', $choreId)->fetch();
return $choreRow !== null;
}
}

View File

@@ -63,4 +63,9 @@ class DatabaseService
return false;
}
public function GetDbChangedTime()
{
return date('Y-m-d H:i:s', filemtime(GROCY_DATAPATH . '/grocy.db'));
}
}

View File

@@ -31,29 +31,36 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Bunch')}', '{$localizationService->Localize('Bunches')}'); --7
INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Gram')}', '{$localizationService->Localize('Grams')}'); --8
INSERT INTO product_groups(name) VALUES ('01 {$localizationService->Localize('Sweets')}'); --1
INSERT INTO product_groups(name) VALUES ('02 {$localizationService->Localize('Bakery products')}'); --2
INSERT INTO product_groups(name) VALUES ('03 {$localizationService->Localize('Tinned food')}'); --3
INSERT INTO product_groups(name) VALUES ('04 {$localizationService->Localize('Butchery products')}'); --4
INSERT INTO product_groups(name) VALUES ('05 {$localizationService->Localize('Vegetables/Fruits')}'); --5
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) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8); --1
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('{$localizationService->Localize('Chocolate')}', 3, 3, 3, 1, 8); --2
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('{$localizationService->Localize('Crisps')}', 3, 3, 3, 1, 10); --4
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Eggs')}', 2, 3, 2, 10); --5
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Noodles')}', 3, 3, 3, 1); --6
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Pickles')}', 4,4, 4, 1); --7
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Gulash soup')}', 4, 5, 5, 1); --8
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Yogurt')}', 2, 6, 6, 1); --9
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Cheese')}', 2, 3, 3, 1); --10
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Cold cuts')}', 2, 3, 3, 1); --11
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Radish')}', 2, 7, 7, 1); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1); --15
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Pizza dough')}', 3, 3, 3, 1); --16
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Sieved tomatoes')}', 4, 5, 5, 1); --17
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Salami')}', 2, 3, 3, 1); --18
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Toast')}', 4, 5, 5, 1); --19
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Minced meat')}', 2, 3, 3, 1); --20
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Flour')}', 2, 3, 3, 1); --21
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Sugar')}', 3, 3, 3, 1); --22
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) 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) 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
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Pickles')}', 4,4, 4, 1, 3); --7
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Gulash soup')}', 4, 5, 5, 1, 3); --8
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) 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) 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
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Toast')}', 4, 5, 5, 1, 2); --19
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Minced meat')}', 2, 3, 3, 1, 4); --20
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Flour')}', 2, 3, 3, 1, 3); --21
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Sugar')}', 3, 3, 3, 1, 3); --22
INSERT INTO shopping_list (note, amount) VALUES ('{$localizationService->Localize('Some good snacks')}', 1);
INSERT INTO shopping_list (product_id, amount) VALUES (20, 1);
@@ -78,15 +85,26 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (4, 21, 200, 8, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (4, 22, 200, 8, 1);
INSERT INTO habits (name, period_type, period_days) VALUES ('{$localizationService->Localize('Changed towels in the bathroom')}', 'manually', 5); --1
INSERT INTO habits (name, period_type, period_days) VALUES ('{$localizationService->Localize('Cleaned the kitchen floor')}', 'dynamic-regular', 7); --2
INSERT INTO habits (name, period_type, period_days) VALUES ('{$localizationService->Localize('Lawn mowed in the garden')}', 'dynamic-regular', 21); --3
INSERT INTO chores (name, period_type, period_days) VALUES ('{$localizationService->Localize('Changed towels in the bathroom')}', 'manually', 5); --1
INSERT INTO chores (name, period_type, period_days) VALUES ('{$localizationService->Localize('Cleaned the kitchen floor')}', 'dynamic-regular', 7); --2
INSERT INTO chores (name, period_type, period_days) VALUES ('{$localizationService->Localize('Lawn mowed in the garden')}', 'dynamic-regular', 21); --3
INSERT INTO batteries (name, description, used_in) VALUES ('{$localizationService->Localize('Battery')}1', '{$localizationService->Localize('Warranty ends')} 2023', '{$localizationService->Localize('TV remote control')}'); --1
INSERT INTO batteries (name, description, used_in) VALUES ('{$localizationService->Localize('Battery')}2', '{$localizationService->Localize('Warranty ends')} 2022', '{$localizationService->Localize('Alarm clock')}'); --2
INSERT INTO batteries (name, description, used_in, charge_interval_days) VALUES ('{$localizationService->Localize('Battery')}3', '{$localizationService->Localize('Warranty ends')} 2022', '{$localizationService->Localize('Heat remote control')}', 60); --3
INSERT INTO batteries (name, description, used_in, charge_interval_days) VALUES ('{$localizationService->Localize('Battery')}4', '{$localizationService->Localize('Warranty ends')} 2028', '{$localizationService->Localize('Heat remote control')}', 60); --4
INSERT INTO task_categories (name) VALUES ('{$localizationService->Localize('Home')}'); --1
INSERT INTO task_categories (name) VALUES ('{$localizationService->Localize('Life')}'); --2
INSERT INTO task_categories (name) VALUES ('{$localizationService->Localize('Projects')}'); --3
INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Repair the garage door')}', 1, date(datetime('now', 'localtime'), '+14 day'), 1);
INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Fork and improve grocy')}', 3, date(datetime('now', 'localtime'), '+30 day'), 1);
INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}1', 2, date(datetime('now', 'localtime'), '-1 day'), 1);
INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}2', 2, date(datetime('now', 'localtime'), '-1 day'), 1);
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 migrations (migration) VALUES (-1);
";
@@ -164,13 +182,13 @@ class DemoDataGeneratorService extends BaseService
$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice());
$stockService->AddMissingProductsToShoppingList();
$habitsService = new HabitsService();
$habitsService->TrackHabit(1, date('Y-m-d H:i:s', strtotime('-5 days')));
$habitsService->TrackHabit(1, date('Y-m-d H:i:s', strtotime('-10 days')));
$habitsService->TrackHabit(1, date('Y-m-d H:i:s', strtotime('-15 days')));
$habitsService->TrackHabit(2, date('Y-m-d H:i:s', strtotime('-10 days')));
$habitsService->TrackHabit(2, date('Y-m-d H:i:s', strtotime('-20 days')));
$habitsService->TrackHabit(3, date('Y-m-d H:i:s', strtotime('-17 days')));
$choresService = new ChoresService();
$choresService->TrackChore(1, date('Y-m-d H:i:s', strtotime('-5 days')));
$choresService->TrackChore(1, date('Y-m-d H:i:s', strtotime('-10 days')));
$choresService->TrackChore(1, date('Y-m-d H:i:s', strtotime('-15 days')));
$choresService->TrackChore(2, date('Y-m-d H:i:s', strtotime('-10 days')));
$choresService->TrackChore(2, date('Y-m-d H:i:s', strtotime('-20 days')));
$choresService->TrackChore(3, date('Y-m-d H:i:s', strtotime('-17 days')));
$batteriesService = new BatteriesService();
$batteriesService->TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-200 days')));

31
services/FilesService.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace Grocy\Services;
class FilesService extends BaseService
{
public function __construct()
{
parent::__construct();
$this->StoragePath = GROCY_DATAPATH . '/storage';
if (!file_exists($this->StoragePath))
{
mkdir($this->StoragePath);
}
}
private $StoragePath;
public function GetFilePath($group, $fileName)
{
$groupFolderPath = $this->StoragePath . '/' . $group;
if (!file_exists($groupFolderPath))
{
mkdir($groupFolderPath);
}
return $groupFolderPath . '/' . $fileName;
}
}

View File

@@ -1,74 +0,0 @@
<?php
namespace Grocy\Services;
class HabitsService extends BaseService
{
const HABIT_TYPE_MANUALLY = 'manually';
const HABIT_TYPE_DYNAMIC_REGULAR = 'dynamic-regular';
public function GetCurrent()
{
$sql = 'SELECT * from habits_current';
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetHabitDetails(int $habitId)
{
if (!$this->HabitExists($habitId))
{
throw new \Exception('Habit does not exist');
}
$habit = $this->Database->habits($habitId);
$habitTrackedCount = $this->Database->habits_log()->where('habit_id', $habitId)->count();
$habitLastTrackedTime = $this->Database->habits_log()->where('habit_id', $habitId)->max('tracked_time');
$nextExeuctionTime = $this->Database->habits_current()->where('habit_id', $habitId)->min('next_estimated_execution_time');
$lastHabitLogRow = $this->Database->habits_log()->where('habit_id = :1 AND tracked_time = :2', $habitId, $habitLastTrackedTime)->fetch();
$lastDoneByUser = null;
if ($lastHabitLogRow !== null && !empty($lastHabitLogRow))
{
$usersService = new UsersService();
$users = $usersService->GetUsersAsDto();
$lastDoneByUser = FindObjectInArrayByPropertyValue($users, 'id', $lastHabitLogRow->done_by_user_id);
}
return array(
'habit' => $habit,
'last_tracked' => $habitLastTrackedTime,
'tracked_count' => $habitTrackedCount,
'last_done_by' => $lastDoneByUser,
'next_estimated_execution_time' => $nextExeuctionTime
);
}
public function TrackHabit(int $habitId, string $trackedTime, $doneBy = GROCY_USER_ID)
{
if (!$this->HabitExists($habitId))
{
throw new \Exception('Habit does not exist');
}
$userRow = $this->Database->users()->where('id = :1', $doneBy)->fetch();
if ($userRow === null)
{
throw new \Exception('User does not exist');
}
$logRow = $this->Database->habits_log()->createRow(array(
'habit_id' => $habitId,
'tracked_time' => $trackedTime,
'done_by_user_id' => $doneBy
));
$logRow->save();
return true;
}
private function HabitExists($habitId)
{
$habitRow = $this->Database->habits()->where('id = :1', $habitId)->fetch();
return $habitRow !== null;
}
}

View File

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

View File

@@ -33,14 +33,20 @@ class SessionService extends BaseService
/**
* @return string
*/
public function CreateSession($userId)
public function CreateSession($userId, $stayLoggedInPermanently = false)
{
$newSessionKey = $this->GenerateSessionKey();
$expires = date('Y-m-d H:i:s', time() + 2592000); // Default is that sessions expire in 30 days
if ($stayLoggedInPermanently === true)
{
$expires = date('Y-m-d H:i:s', time() + 31220640000); // 999 years aka forever
}
$sessionRow = $this->Database->sessions()->createRow(array(
'user_id' => $userId,
'session_key' => $newSessionKey,
'expires' => date('Y-m-d H:i:s', time() + 2592000) // Default is that sessions expire in 30 days
'expires' => $expires
));
$sessionRow->save();

34
services/TasksService.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace Grocy\Services;
class TasksService extends BaseService
{
public function GetCurrent()
{
$sql = 'SELECT * from tasks_current';
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function MarkTaskAsCompleted($taskId, $doneTime)
{
if (!$this->TaskExists($taskId))
{
throw new \Exception('Task does not exist');
}
$taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch();
$taskRow->update(array(
'done' => 1,
'done_timestamp' => $doneTime
));
return true;
}
private function TaskExists($taskId)
{
$taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch();
return $taskRow !== null;
}
}

View File

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

View File

@@ -1,4 +1,4 @@
{
"Version": "1.18.0",
"ReleaseDate": "2018-08-11"
"Version": "1.20.0",
"ReleaseDate": "2018-09-30"
}

View File

@@ -12,12 +12,20 @@
<div class="row">
<div class="col">
<h1>@yield('title')</h1>
<p id="info-due-batteries" data-next-x-days="{{ $nextXDays }}" class="btn btn-lg btn-warning no-real-button responsive-button mr-2"></p>
<p id="info-overdue-batteries" class="btn btn-lg btn-danger no-real-button responsive-button"></p>
<p id="info-due-batteries" data-status-filter="duesoon" data-next-x-days="{{ $nextXDays }}" class="btn btn-lg btn-warning status-filter-button responsive-button mr-2"></p>
<p id="info-overdue-batteries" data-status-filter="overdue" class="btn btn-lg btn-danger status-filter-button responsive-button"></p>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="status-filter">{{ $L('Filter by status') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="status-filter">
<option class="bg-white" value="all">{{ $L('All') }}</option>
<option class="bg-warning" value="duesoon">{{ $L('Due soon') }}</option>
<option class="bg-danger" value="overdue">{{ $L('Overdue') }}</option>
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
@@ -33,13 +41,14 @@
<th>{{ $L('Battery') }}</th>
<th>{{ $L('Last charged') }}</th>
<th>{{ $L('Next planned charge cycle') }}</th>
<th class="d-none">Hidden status</th>
</tr>
</thead>
<tbody>
@foreach($current as $curentBatteryEntry)
<tr id="battery-{{ $curentBatteryEntry->battery_id }}-row" class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) table-warning @endif">
<td class="fit-content">
<a class="btn btn-success btn-sm track-charge-cycle-button" href="#" data-toggle="tooltip" title="{{ $L('Track charge cycle of battery #1', FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name) }}"
<a class="btn btn-success btn-sm track-charge-cycle-button" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Track charge cycle of battery #1', FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name) }}"
data-battery-id="{{ $curentBatteryEntry->battery_id }}"
data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}">
<i class="fas fa-fire"></i>
@@ -60,6 +69,9 @@
...
@endif
</td>
<td class="d-none">
"@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $curentBatteryEntry->next_estimated_charge_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif
</td>
</tr>
@endforeach
</tbody>

View File

@@ -37,13 +37,17 @@
<input type="text" class="form-control" id="used_in" name="used_in" value="@if($mode == 'edit'){{ $battery->used_in }}@endif">
</div>
<div class="form-group">
<label for="charge_interval_days">{{ $L('Charge cycle interval (days)') }}<br><span class="small text-muted">{{ $L('0 means suggestions for the next charge cycle are disabled') }}</span></label>
<input required min="0" step="1" type="number" class="form-control" id="charge_interval_days" name="charge_interval_days" value="@if($mode == 'edit'){{ $battery->charge_interval_days }}@else{{0}}@endif">
<div class="invalid-feedback">{{ $L('This cannot be negative') }}</div>
</div>
@php if($mode == 'edit') { $value = $battery->charge_interval_days; } else { $value = 0; } @endphp
@include('components.numberpicker', array(
'id' => 'charge_interval_days',
'label' => 'Charge cycle interval (days)',
'value' => $value,
'min' => '0',
'hint' => $L('0 means suggestions for the next charge cycle are disabled'),
'invalidFeedback' => $L('This cannot be negative')
))
<button id="save-battery-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-battery-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

View File

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

View File

@@ -1,12 +1,12 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $L('Edit habit'))
@section('title', $L('Edit chore'))
@else
@section('title', $L('Create habit'))
@section('title', $L('Create chore'))
@endif
@section('viewJsName', 'habitform')
@section('viewJsName', 'choreform')
@section('content')
<div class="row">
@@ -16,41 +16,44 @@
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $habit->id }};</script>
<script>Grocy.EditObjectId = {{ $chore->id }};</script>
@endif
<form id="habit-form" novalidate>
<form id="chore-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'){{ $habit->name }}@endif">
<input type="text" class="form-control" required id="name" name="name" value="@if($mode == 'edit'){{ $chore->name }}@endif">
<div class="invalid-feedback">{{ $L('A name is required') }}</div>
</div>
<div class="form-group">
<label for="description">{{ $L('Description') }}</label>
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $habit->description }}@endif</textarea>
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $chore->description }}@endif</textarea>
</div>
<div class="form-group">
<label for="period_type">{{ $L('Period type') }}</label>
<select required class="form-control input-group-habit-period-type" id="period_type" name="period_type">
<select required class="form-control input-group-chore-period-type" id="period_type" name="period_type">
@foreach($periodTypes as $periodType)
<option @if($mode == 'edit' && $periodType == $habit->period_type) selected="selected" @endif value="{{ $periodType }}">{{ $L($periodType) }}</option>
<option @if($mode == 'edit' && $periodType == $chore->period_type) selected="selected" @endif value="{{ $periodType }}">{{ $L($periodType) }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $L('A period type is required') }}</div>
</div>
<div class="form-group">
<label for="period_days">{{ $L('Period days') }}</label>
<input type="number" class="form-control input-group-habit-period-type" id="period_days" name="period_days" min="0" value="@if($mode == 'edit'){{ $habit->period_days }}@endif">
<div class="invalid-feedback">{{ $L('This cannot be negative') }}</div>
</div>
@php if($mode == 'edit') { $value = $chore->period_days; } else { $value = 0; } @endphp
@include('components.numberpicker', array(
'id' => 'period_days',
'label' => 'Period days',
'value' => $value,
'min' => '0',
'additionalCssClasses' => 'input-group-chore-period-type',
'invalidFeedback' => $L('This cannot be negative'),
'additionalHtmlElements' => '<p id="chore-period-type-info" class="form-text text-muted small d-none"></p>'
))
<p id="habit-period-type-info" class="form-text text-muted small d-none"></p>
<button id="save-habit-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
<button id="save-chore-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>

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