From 83624eaf27df3528eca6c2f2d99aced1b32d7323 Mon Sep 17 00:00:00 2001 From: Marius Boro Date: Sun, 30 Sep 2018 09:22:30 +0200 Subject: [PATCH 01/46] Update no.php (#75) --- localization/no.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/localization/no.php b/localization/no.php index 07900fa5..d0f3ef7f 100644 --- a/localization/no.php +++ b/localization/no.php @@ -12,7 +12,7 @@ return array( 'Chores overview' => 'Oversikt Husarbeid', 'Batteries overview' => 'Oversikt Batteri', 'Purchase' => 'Innkjøp', - 'Consume' => 'Forbrukt', + 'Consume' => 'Forbruk produkt', 'Inventory' => 'Endre Husholdning', 'Shopping list' => 'Handleliste', 'Chore tracking' => 'Logge Husarbeid', @@ -124,7 +124,7 @@ return array( 'Stock amount of #1 is now #2 #3' => 'Husholdning antall #1 er nå #2 #3', 'Tracked execution of chore #1 on #2' => 'Utførte husarbeid oppgave "#1" den #2', 'Tracked charge cycle of battery #1 on #2' => 'Ladet #1 den #2', - 'Consume all #1 which are currently in stock' => 'Konsumér alle #1 som er i husholdningen', + 'Consume all #1 which are currently in stock' => 'Forbruk alle #1 som er i husholdningen', 'All' => 'Alle', 'Track charge cycle of battery #1' => '#1 ladet', 'Track execution of chore #1' => 'Utfør husarbeid oppgave #1', @@ -212,7 +212,7 @@ return array( 'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Huk av hvis du ønsker å bruke mindre enn forpakningsstørrelse i husholdningen', 'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "bruke mindre enn forpakningsstørrelse i husholdningen" blir ignorert', 'Removed all ingredients of recipe "#1" from stock' => 'Fjern alle ingredienser for "#1" oppskriften fra husholdningen.', - 'Consume all ingredients needed by this recipe' => 'Konsumer alle ingredienser for denne oppskriften', + 'Consume all ingredients needed by this recipe' => 'Forbruk alle ingredienser for denne oppskriften', 'Click to show technical details' => 'Klikk for å vise teknisk informasjon', 'Error while saving, probably this item already exists' => 'Kunne ikke lagre, produkt er lagt til fra før', 'Error details' => 'Detaljer om feil', From be326a5211d35d94ff72857f2a8c6a59d5e7b81d Mon Sep 17 00:00:00 2001 From: Talmai Oliveira Date: Sun, 30 Sep 2018 03:31:16 -0400 Subject: [PATCH 02/46] 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 --- .dockerignore | 6 ++++ Dockerfile-grocy | 58 ++++++++++++++++++++++++++++++++ Dockerfile-grocy-nginx | 32 ++++++++++++++++++ README.md | 12 ++++++- config-dist.php | 2 +- docker-compose.yml | 30 +++++++++++++++++ docker_nginx/common.conf | 28 +++++++++++++++ docker_nginx/conf.d/default.conf | 8 +++++ docker_nginx/conf.d/ssl.conf | 20 +++++++++++ docker_nginx/nginx.conf | 42 +++++++++++++++++++++++ info.php | 6 ++++ 11 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile-grocy create mode 100644 Dockerfile-grocy-nginx create mode 100644 docker-compose.yml create mode 100644 docker_nginx/common.conf create mode 100644 docker_nginx/conf.d/default.conf create mode 100644 docker_nginx/conf.d/ssl.conf create mode 100644 docker_nginx/nginx.conf create mode 100644 info.php diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3aa67ef9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.vscode +.gitignore +build.bat +Dockerfile +.DS_store \ No newline at end of file diff --git a/Dockerfile-grocy b/Dockerfile-grocy new file mode 100644 index 00000000..2f30b4e0 --- /dev/null +++ b/Dockerfile-grocy @@ -0,0 +1,58 @@ +FROM php:7.2-fpm-alpine +MAINTAINER Talmai Oliveira + +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 \ No newline at end of file diff --git a/Dockerfile-grocy-nginx b/Dockerfile-grocy-nginx new file mode 100644 index 00000000..727fffb2 --- /dev/null +++ b/Dockerfile-grocy-nginx @@ -0,0 +1,32 @@ +FROM alpine:latest +MAINTAINER Talmai Oliveira + +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;"] \ No newline at end of file diff --git a/README.md b/README.md index 191d80a2..1dfd3165 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ERP beyond your fridge - Public demo of the latest pre-release version (current master branch) → [https://demo-prerelease.grocy.info](https://demo-prerelease.grocy.info) ## Motivation -A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. ERP your fridge! +A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge! ## How to install > **NEW** @@ -23,6 +23,16 @@ If you use nginx as your webserver, please include `try_files $uri /index.php;` If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php` (`Setting('DISABLE_URL_REWRITING', true);`). +## How to run using Docker + +The docker images build are based on [Alpine](https://hub.docker.com/_/alpine/), with an extremelly low footprint (less than 10 MB for nginx, and less than 70MB for grocy with php-fm. That number is eventually bumped up to 353MB after all the dependencies are downloaded, however). Anyhow, to run using docker just do the following: + +``` +> docker-compose up +``` + +And grocy should be accessible via `http(s)://localhost/`. The https option will work. However, since the certificate is self-signed, most browsers will complain. + ## How to update Just overwrite everything with the latest release while keeping the `data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings). Just to be sure, please empty `data/viewcache`. diff --git a/config-dist.php b/config-dist.php index 9428a092..24c4007a 100644 --- a/config-dist.php +++ b/config-dist.php @@ -7,7 +7,7 @@ Setting('MODE', 'production'); # one of the other available localization files in the "/localization" directory Setting('CULTURE', 'en'); -# To keep it simpel, grocy does not handle any currency conversions, +# To keep it simple: grocy does not handle any currency conversions, # this here is used to format all money values, # so can be anything (e. g. "USD" OR "$", doesn't matter...) Setting('CURRENCY', '$'); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..11845672 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/docker_nginx/common.conf b/docker_nginx/common.conf new file mode 100644 index 00000000..34c75204 --- /dev/null +++ b/docker_nginx/common.conf @@ -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; +} \ No newline at end of file diff --git a/docker_nginx/conf.d/default.conf b/docker_nginx/conf.d/default.conf new file mode 100644 index 00000000..40b8b2b2 --- /dev/null +++ b/docker_nginx/conf.d/default.conf @@ -0,0 +1,8 @@ +server { + listen 80 default_server; + server_name _; + + root /www/public; # see: volumes_from + + include /etc/nginx/common.conf; +} \ No newline at end of file diff --git a/docker_nginx/conf.d/ssl.conf b/docker_nginx/conf.d/ssl.conf new file mode 100644 index 00000000..65385444 --- /dev/null +++ b/docker_nginx/conf.d/ssl.conf @@ -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; + +} \ No newline at end of file diff --git a/docker_nginx/nginx.conf b/docker_nginx/nginx.conf new file mode 100644 index 00000000..32b74a43 --- /dev/null +++ b/docker_nginx/nginx.conf @@ -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; +} diff --git a/info.php b/info.php new file mode 100644 index 00000000..976ac140 --- /dev/null +++ b/info.php @@ -0,0 +1,6 @@ + \ No newline at end of file From 77d82f22dc7b5a3f4a51e4c995d963d0eabdee7c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 09:41:22 +0200 Subject: [PATCH 03/46] Fixed scrolling did not work when showing a recipe in fullscreen mode (fixes #76) --- public/css/grocy.css | 13 +++++++------ public/viewjs/recipes.js | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/public/css/grocy.css b/public/css/grocy.css index 97f4fa8d..93802c5e 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -50,12 +50,13 @@ a.discrete-link:focus { } .fullscreen { - z-index: 9999; - width: 100%; - height: 100%; - position: fixed; - top: 0; - left: 0; + z-index: 9999; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + overflow: auto; } .form-check-input.is-valid ~ .form-check-label, diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index b2fa6117..cb11ad21 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -158,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"); }); From d11dcb38fe1d2e70a31449fa3a590eca458facf1 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 09:57:42 +0200 Subject: [PATCH 04/46] Only reload the page on external changes when there is no unsaved form data (fixes #73) --- public/js/grocy.js | 9 +++++++++ public/js/grocy_dbchangedhandling.js | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/public/js/grocy.js b/public/js/grocy.js index 24b8fd29..45edcc8b 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -191,3 +191,12 @@ Grocy.FrontendHelpers.ShowGenericError = function(message, exception) console.error(exception); } + +$("form").on("keyup paste", "input, textarea", function() +{ + $(this).closest("form").addClass("is-dirty"); +}); +$("form").on("click", "select", function() +{ + $(this).closest("form").addClass("is-dirty"); +}); diff --git a/public/js/grocy_dbchangedhandling.js b/public/js/grocy_dbchangedhandling.js index e35fa2cd..d32b6231 100644 --- a/public/js/grocy_dbchangedhandling.js +++ b/public/js/grocy_dbchangedhandling.js @@ -11,6 +11,7 @@ // 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 +// and when there is no unsaved form data setInterval(function() { Grocy.Api.Get('system/get-db-changed-time', @@ -21,7 +22,10 @@ setInterval(function() { if (Grocy.IdleTime >= 50) { - window.location.reload(); + if ($("form.is-dirty").length === 0) + { + window.location.reload(); + } } Grocy.DatabaseChangedTime = newDbChangedTime; From b81316bd60b3ff96a0bd5add73bbf0a1fbf8dece Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 10:13:37 +0200 Subject: [PATCH 05/46] Include products which are not in stock currently but below min. stock amount on stock overview page (fixes #69) --- migrations/0038.sql | 27 +++++++++++++++++++++++++++ views/stockoverview.blade.php | 8 ++++---- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 migrations/0038.sql diff --git a/migrations/0038.sql b/migrations/0038.sql new file mode 100644 index 00000000..3595c9fd --- /dev/null +++ b/migrations/0038.sql @@ -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; diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index b48e92f8..362ffcea 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -58,16 +58,16 @@ @foreach($currentStock as $currentStockEntry) - product_id) !== null) table-info @endif"> + amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif"> - 1 - product_id)->location_id)->name }} - @if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days"))) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif + @if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif @endforeach From 0bbd2d9880c2706c2dafcd886e82cd3c8bdfc0ae Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 10:47:56 +0200 Subject: [PATCH 06/46] Prepare user settings API (references #74 and #71) --- controllers/UsersApiController.php | 28 +++++++++ grocy.openapi.json | 99 ++++++++++++++++++++++++++++++ migrations/0039.sql | 10 +++ routes.php | 4 ++ services/UsersService.php | 34 ++++++++++ 5 files changed, 175 insertions(+) create mode 100644 migrations/0039.sql diff --git a/controllers/UsersApiController.php b/controllers/UsersApiController.php index 4b1b10e0..9afa7475 100644 --- a/controllers/UsersApiController.php +++ b/controllers/UsersApiController.php @@ -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()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index fcb0883f..aa6cff46 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -617,6 +617,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", @@ -2098,6 +2189,14 @@ "format": "date-time" } } + }, + "UserSetting": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } } }, "examples": { diff --git a/migrations/0039.sql b/migrations/0039.sql new file mode 100644 index 00000000..7f8e9c80 --- /dev/null +++ b/migrations/0039.sql @@ -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) +); diff --git a/routes.php b/routes.php index 57814751..28794694 100644 --- a/routes.php +++ b/routes.php @@ -91,6 +91,10 @@ $app->group('/api', function() $this->post('/users/edit/{userId}', '\Grocy\Controllers\UsersApiController:EditUser'); $this->get('/users/delete/{userId}', '\Grocy\Controllers\UsersApiController:DeleteUser'); + // User + $this->get('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:GetUserSetting'); + $this->post('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:SetUserSetting'); + // Stock $this->get('/stock/add-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:AddProduct'); $this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct'); diff --git a/services/UsersService.php b/services/UsersService.php index 1bc3f74b..1200f53a 100644 --- a/services/UsersService.php +++ b/services/UsersService.php @@ -50,6 +50,40 @@ 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 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(); From d4227d2e416b521ab38a73a81406e55bd22cd00f Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 11:17:28 +0200 Subject: [PATCH 07/46] Make auto reloading the page on external database changes configurable (closes #74) --- controllers/BaseController.php | 10 +++++++++ public/js/grocy_dbchangedhandling.js | 32 +++++++++++++++++++++++++--- services/UsersService.php | 13 +++++++++++ views/layout/default.blade.php | 14 ++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/controllers/BaseController.php b/controllers/BaseController.php index b0a10d4c..57e81893 100644 --- a/controllers/BaseController.php +++ b/controllers/BaseController.php @@ -5,6 +5,7 @@ namespace Grocy\Controllers; use \Grocy\Services\DatabaseService; use \Grocy\Services\ApplicationService; use \Grocy\Services\LocalizationService; +use \Grocy\Services\UsersService; class BaseController { @@ -41,6 +42,15 @@ class BaseController return $container->UrlManager->ConstructUrl($relativePath, $isResource); }); + try { + $usersService = new UsersService(); + $container->view->set('userSettings', $usersService->GetUserSettings(GROCY_USER_ID)); + } + catch (\Exception $ex) + { + // Happens when database is not initialised or migrated... + } + $this->AppContainer = $container; } diff --git a/public/js/grocy_dbchangedhandling.js b/public/js/grocy_dbchangedhandling.js index d32b6231..b2261fb6 100644 --- a/public/js/grocy_dbchangedhandling.js +++ b/public/js/grocy_dbchangedhandling.js @@ -10,8 +10,8 @@ ); // Check if the database has changed once a minute -// If a change is detected, reload the current page, but only if already idling for at least 50 seconds -// and when there is no unsaved form data +// 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', @@ -22,7 +22,7 @@ setInterval(function() { if (Grocy.IdleTime >= 50) { - if ($("form.is-dirty").length === 0) + if (Grocy.AutoReloadOnDatabaseChangeEnabled && $("form.is-dirty").length === 0) { window.location.reload(); } @@ -55,3 +55,29 @@ setInterval(function() { Grocy.IdleTime += 1; }, 1000); + +$("#auto-reload-enabled").on("change", function() +{ + var value = $(this).is(":checked"); + + Grocy.AutoReloadOnDatabaseChangeEnabled = value; + + jsonData = { }; + jsonData.value = value; + console.log(jsonData); + Grocy.Api.Post('user/settings/auto_reload_on_db_change', jsonData, + function(result) + { + // Nothing to do... + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); +}); + +if (Grocy.AutoReloadOnDatabaseChangeEnabled) +{ + $("#auto-reload-enabled").prop("checked", true); +} diff --git a/services/UsersService.php b/services/UsersService.php index 1200f53a..cf5b4965 100644 --- a/services/UsersService.php +++ b/services/UsersService.php @@ -63,6 +63,19 @@ class UsersService extends BaseService } } + 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; + } + + return $settings; + } + public function SetUserSetting($userId, $settingKey, $settingValue) { $settingRow = $this->Database->user_settings()->where('user_id = :1 AND key = :2', $userId, $settingKey)->fetch(); diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 1a408c7f..ac56b0b0 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -41,6 +41,12 @@ Grocy.ActiveNav = '@yield('activeNav', '')'; Grocy.Culture = '{{ GROCY_CULTURE }}'; Grocy.Currency = '{{ GROCY_CURRENCY }}'; + + @if(array_key_exists('auto_reload_on_db_change', $userSettings)) + Grocy.AutoReloadOnDatabaseChangeEnabled = {{ BoolToString($userSettings['auto_reload_on_db_change']) }}; + @else + Grocy.AutoReloadOnDatabaseChangeEnabled = true; + @endif @@ -191,6 +197,14 @@ From 756ec319cce412381fd3f476a3184cc209c17f24 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 20:11:24 +0200 Subject: [PATCH 15/46] Update dependencies for next release --- composer.lock | 42 +++++++++++++++++++++--------------------- version.json | 4 ++-- yarn.lock | 8 ++++---- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/composer.lock b/composer.lock index 18c86b17..6c2f0fcf 100644 --- a/composer.lock +++ b/composer.lock @@ -159,7 +159,7 @@ }, { "name": "illuminate/container", - "version": "v5.7.5", + "version": "v5.7.6", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", @@ -203,7 +203,7 @@ }, { "name": "illuminate/contracts", - "version": "v5.7.5", + "version": "v5.7.6", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -247,7 +247,7 @@ }, { "name": "illuminate/events", - "version": "v5.7.5", + "version": "v5.7.6", "source": { "type": "git", "url": "https://github.com/illuminate/events.git", @@ -292,7 +292,7 @@ }, { "name": "illuminate/filesystem", - "version": "v5.7.5", + "version": "v5.7.6", "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", @@ -344,7 +344,7 @@ }, { "name": "illuminate/support", - "version": "v5.7.5", + "version": "v5.7.6", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", @@ -403,7 +403,7 @@ }, { "name": "illuminate/view", - "version": "v5.7.5", + "version": "v5.7.6", "source": { "type": "git", "url": "https://github.com/illuminate/view.git", @@ -1167,16 +1167,16 @@ }, { "name": "symfony/debug", - "version": "v4.1.4", + "version": "v4.1.5", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "47ead688f1f2877f3f14219670f52e4722ee7052" + "reference": "b4a0b67dee59e2cae4449a8f8eabc508d622fd33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/47ead688f1f2877f3f14219670f52e4722ee7052", - "reference": "47ead688f1f2877f3f14219670f52e4722ee7052", + "url": "https://api.github.com/repos/symfony/debug/zipball/b4a0b67dee59e2cae4449a8f8eabc508d622fd33", + "reference": "b4a0b67dee59e2cae4449a8f8eabc508d622fd33", "shasum": "" }, "require": { @@ -1219,20 +1219,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-08-03T11:13:38+00:00" + "time": "2018-09-22T19:04:12+00:00" }, { "name": "symfony/finder", - "version": "v4.1.4", + "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": { @@ -1268,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", @@ -1331,16 +1331,16 @@ }, { "name": "symfony/translation", - "version": "v4.1.4", + "version": "v4.1.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f" + "reference": "6e49130ddf150b7bfe9e34edb2f3f698aa1aa43b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/fa2182669f7983b7aa5f1a770d053f79f0ef144f", - "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f", + "url": "https://api.github.com/repos/symfony/translation/zipball/6e49130ddf150b7bfe9e34edb2f3f698aa1aa43b", + "reference": "6e49130ddf150b7bfe9e34edb2f3f698aa1aa43b", "shasum": "" }, "require": { @@ -1396,7 +1396,7 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-08-07T12:45:11+00:00" + "time": "2018-09-21T12:49:42+00:00" }, { "name": "tuupola/callable-handler", diff --git a/version.json b/version.json index 8d574d12..4b155bd0 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "Version": "1.19.2", - "ReleaseDate": "2018-09-29" + "Version": "1.20.0", + "ReleaseDate": "2018-09-30" } diff --git a/yarn.lock b/yarn.lock index 5fb5c550..02957e0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4,13 +4,13 @@ "@danielfarrell/bootstrap-combobox@https://github.com/berrnd/bootstrap-combobox.git#master": version "1.1.8" - resolved "https://github.com/berrnd/bootstrap-combobox.git#d5a43b011d4d2c86537df26e15d2caa51be6a15f" + resolved "https://github.com/berrnd/bootstrap-combobox.git#fcf0110146f4daab94888234c57d198b4ca5f129" "@fortawesome/fontawesome-free@^5.1.0": version "5.3.1" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.3.1.tgz#5466b8f31c1f493a96754c1426c25796d0633dd9" -"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2", "tagmanager@https://github.com/max-favilli/tagmanager.git#3.0.2": +"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2": version "3.0.1" resolved "https://github.com/max-favilli/tagmanager.git#df9eb9935c8585a392dfc00602f890caf233fa94" dependencies: @@ -203,8 +203,8 @@ startbootstrap-sb-admin@^4.0.0: jquery.easing "^1.4.1" swagger-ui-dist@^3.17.3: - version "3.19.0" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.0.tgz#95942ce1a556e7fe2705d7c92c6004a628d53207" + version "3.19.1" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.1.tgz#ee176cfb070470bb81ba98a686e808035a882cb1" tempusdominus-bootstrap-4@^5.0.1: version "5.1.1" From 6c74881f957cd00635e73bd7478a788a459c59c8 Mon Sep 17 00:00:00 2001 From: Marius Boro Date: Sun, 30 Sep 2018 21:40:26 +0200 Subject: [PATCH 16/46] Update no.php (#79) --- localization/no.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/localization/no.php b/localization/no.php index d0f3ef7f..e3fae8ba 100644 --- a/localization/no.php +++ b/localization/no.php @@ -249,7 +249,15 @@ return array( 'Already expired' => 'Utgått på dato', 'Due soon' => 'Forfaller snart', 'Overdue' => 'Forfalt', - + 'View settings' => 'xxx', + 'Auto reload on external changes' => 'Automatisk fornying ved ekstern endring', + 'Enable night mode' => 'Aktiver nattmodus', + 'Auto enable in time range' => 'Automatisk aktivering i tidsrommet', + 'From' => 'Fra', + 'in format' => 'format', + 'To' => 'Til', + 'Time range goes over midnight' => 'Tidsrommet går over midnatt', + //Constants 'manually' => 'Manuel', 'dynamic-regular' => 'Automatisk', From c675b534ef9b9aeab28d05554981aa59534987e8 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 21:41:30 +0200 Subject: [PATCH 17/46] Fix missing german translation --- localization/de.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localization/de.php b/localization/de.php index 4346acfc..9cbbe54b 100644 --- a/localization/de.php +++ b/localization/de.php @@ -249,7 +249,7 @@ return array( 'Already expired' => 'Bereits abgelaufen', 'Due soon' => 'Bald fällig', 'Overdue' => 'Überfällig', - 'View settings' => 'xxx', + 'View settings' => 'Ansichtseinstellungen', 'Auto reload on external changes' => 'Autom. akt. bei externen Änderungen', 'Enable night mode' => 'Nachtmodus aktivieren', 'Auto enable in time range' => 'Autom. akt. in diesem Zeitraum', From e5fb609c8eadcbe6f5e5e9999623807496f12b5c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 22:16:33 +0200 Subject: [PATCH 18/46] Finalize file API (references #58) --- controllers/FilesApiController.php | 34 ++++++++++++++++++++ grocy.openapi.json | 51 ++++++++++++++++++++++++++++++ middleware/JsonMiddleware.php | 10 +++++- routes.php | 1 + 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php index 91192e47..656ecc09 100644 --- a/controllers/FilesApiController.php +++ b/controllers/FilesApiController.php @@ -29,6 +29,40 @@ class FilesApiController extends BaseApiController $data = $request->getBody()->getContents(); file_put_contents($this->FilesService->GetFilePath($args['group'], $fileName), $data); + + return $this->ApiResponse(array('success' => true)); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } + } + + public function ServeFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name'])) + { + $fileName = $request->getQueryParams()['file_name']; + } + else + { + throw new \Exception('file_name query parameter missing or contains an invalid filename'); + } + + $filePath = $this->FilesService->GetFilePath($args['group'], $fileName); + + if (file_exists($filePath)) + { + $response->write(file_get_contents($filePath)); + $response = $response->withHeader('Content-Type', mime_content_type($filePath)); + return $response->withHeader('Content-Disposition', 'inline; filename="' . $fileName . '"'); + } + else + { + return $this->VoidApiActionResponse($response, false, 404, 'File not found'); + } } catch (\Exception $ex) { diff --git a/grocy.openapi.json b/grocy.openapi.json index e2bbee91..d50fd8f2 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -491,6 +491,57 @@ } } }, + "/files/get/{group}": { + "get": { + "description": "Serves the given file", + "tags": [ + "Files" + ], + "parameters": [ + { + "in": "path", + "name": "group", + "required": true, + "description": "The file group", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "file_name", + "required": true, + "description": "The file name (including extension)", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The binary file contents (mime type is set based on file extension)", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + } + }, "/users/get": { "get": { "description": "Returns all users", diff --git a/middleware/JsonMiddleware.php b/middleware/JsonMiddleware.php index a20f0ed9..9e7cb635 100644 --- a/middleware/JsonMiddleware.php +++ b/middleware/JsonMiddleware.php @@ -7,6 +7,14 @@ class JsonMiddleware extends BaseMiddleware public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next) { $response = $next($request, $response); - return $response->withHeader('Content-Type', 'application/json'); + + if ($response->hasHeader('Content-Disposition')) + { + return $response; + } + else + { + return $response->withHeader('Content-Type', 'application/json'); + } } } diff --git a/routes.php b/routes.php index 25404d49..873b899e 100644 --- a/routes.php +++ b/routes.php @@ -85,6 +85,7 @@ $app->group('/api', function() // Files $this->post('/files/upload/{group}', '\Grocy\Controllers\FilesApiController:Upload'); + $this->get('/files/get/{group}', '\Grocy\Controllers\FilesApiController:ServeFile'); // Users $this->get('/users/get', '\Grocy\Controllers\UsersApiController:GetUsers'); From 04f34ea6b0f54a4fe0ba20ad737c7e4d01d7f788 Mon Sep 17 00:00:00 2001 From: Marius Boro Date: Sun, 30 Sep 2018 22:17:15 +0200 Subject: [PATCH 19/46] Update no.php (#80) --- localization/no.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localization/no.php b/localization/no.php index e3fae8ba..3a9c094c 100644 --- a/localization/no.php +++ b/localization/no.php @@ -249,7 +249,7 @@ return array( 'Already expired' => 'Utgått på dato', 'Due soon' => 'Forfaller snart', 'Overdue' => 'Forfalt', - 'View settings' => 'xxx', + 'View settings' => 'Se instillinger', 'Auto reload on external changes' => 'Automatisk fornying ved ekstern endring', 'Enable night mode' => 'Aktiver nattmodus', 'Auto enable in time range' => 'Automatisk aktivering i tidsrommet', From 44cd26ae77cdb8ea3e5b1dc7abc2110c2c7e16f5 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 30 Sep 2018 23:22:17 +0200 Subject: [PATCH 20/46] Finish first early version of "pictures for products" (references #58) --- migrations/0040.sql | 2 ++ public/js/grocy.js | 36 ++++++++++++++++++++++++++ public/viewjs/productform.js | 46 +++++++++++++++++++++++++++++++--- public/viewjs/stockoverview.js | 11 ++++++++ views/productform.blade.php | 10 ++++++++ views/stockoverview.blade.php | 5 ++++ 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 migrations/0040.sql diff --git a/migrations/0040.sql b/migrations/0040.sql new file mode 100644 index 00000000..8abf173f --- /dev/null +++ b/migrations/0040.sql @@ -0,0 +1,2 @@ +ALTER TABLE products +ADD picture_file_name TEXT; diff --git a/public/js/grocy.js b/public/js/grocy.js index 4aa2acc7..3b21527d 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -177,6 +177,42 @@ Grocy.Api.Post = function(apiFunction, jsonData, success, error) xhr.send(JSON.stringify(jsonData)); }; +Grocy.Api.UploadFile = function(fileInput, group, success, error) +{ + if (fileInput[0].files.length === 0) + { + return; + } + + var xhr = new XMLHttpRequest(); + var url = U('/api/files/upload/' + group + '?file_name=' + encodeURIComponent(fileInput[0].files[0].name)); + + xhr.onreadystatechange = function() + { + if (xhr.readyState === XMLHttpRequest.DONE) + { + if (xhr.status === 200) + { + if (success) + { + success(JSON.parse(xhr.responseText)); + } + } + else + { + if (error) + { + error(xhr); + } + } + } + }; + + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-type', 'application/octet-stream'); + xhr.send(fileInput[0].files[0]); +}; + Grocy.FrontendHelpers = { }; Grocy.FrontendHelpers.ValidateForm = function(formId) { diff --git a/public/viewjs/productform.js b/public/viewjs/productform.js index d9a053d3..59a2ce1a 100644 --- a/public/viewjs/productform.js +++ b/public/viewjs/productform.js @@ -9,12 +9,34 @@ redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val()); } + var jsonData = $('#product-form').serializeJSON(); + if ($("#product-picture")[0].files.length > 0) + { + jsonData.picture_file_name = $("#product-picture")[0].files[0].name; + } + if (Grocy.EditMode === 'create') { - Grocy.Api.Post('add-object/products', $('#product-form').serializeJSON(), + Grocy.Api.Post('add-object/products', jsonData, function(result) { - window.location.href = redirectDestination; + if (jsonData.hasOwnProperty("picture_file_name")) + { + Grocy.Api.UploadFile($("#product-picture"), 'productpictures', + function(result) + { + window.location.href = redirectDestination; + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + window.location.href = redirectDestination; + } }, function(xhr) { @@ -24,10 +46,26 @@ } else { - Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(), + Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, jsonData, function(result) { - window.location.href = redirectDestination; + if (jsonData.hasOwnProperty("picture_file_name")) + { + Grocy.Api.UploadFile($("#product-picture"), 'productpictures', + function(result) + { + window.location.href = redirectDestination; + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + window.location.href = redirectDestination; + } }, function(xhr) { diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index ddc3f9df..06c1772e 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -141,6 +141,17 @@ $(document).on('click', '.product-consume-button', function(e) ); }); +$(document).on("click", ".show-product-picture-button", function(e) +{ + var pictureUrl = $(e.currentTarget).attr("data-picture-url"); + var productName = $(e.currentTarget).attr("data-product-name"); + + bootbox.alert({ + title: L("Image of product #1", productName), + message: "" + }); +}); + function RefreshStatistics() { Grocy.Api.Get('stock/get-current-stock', diff --git a/views/productform.blade.php b/views/productform.blade.php index fceec874..c062ca43 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -108,6 +108,16 @@ 'additionalHtmlElements' => '

' )) +
+ + + + @if(!empty($product->picture_file_name)) + + + @endif +
+ diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 362ffcea..d2b57afa 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -74,6 +74,11 @@ data-consume-amount="{{ $currentStockEntry->amount }}"> {{ $L('All') }} + + + {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} From f1fc0ee54921d5a528bca131c7cc365fce6df17c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 1 Oct 2018 20:20:50 +0200 Subject: [PATCH 21/46] Finished first version of "pictures for products" (references #58) --- controllers/FilesApiController.php | 29 +++++++++- grocy.openapi.json | 73 +++++++++++++++++++++---- helpers/extensions.php | 2 +- public/js/extensions.js | 10 ++++ public/js/grocy.js | 56 ++++++++++++++++--- public/viewjs/components/productcard.js | 12 ++++ public/viewjs/productform.js | 49 +++++++++++++---- public/viewjs/stockoverview.js | 19 ++++--- routes.php | 5 +- views/components/productcard.blade.php | 4 ++ views/productform.blade.php | 29 +++++++--- views/stockoverview.blade.php | 20 ++++--- 12 files changed, 252 insertions(+), 56 deletions(-) diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php index 656ecc09..4bef5531 100644 --- a/controllers/FilesApiController.php +++ b/controllers/FilesApiController.php @@ -14,7 +14,7 @@ class FilesApiController extends BaseApiController protected $FilesService; - public function Upload(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + public function UploadFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { try { @@ -69,4 +69,31 @@ class FilesApiController extends BaseApiController return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); } } + + public function DeleteFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name'])) + { + $fileName = $request->getQueryParams()['file_name']; + } + else + { + throw new \Exception('file_name query parameter missing or contains an invalid filename'); + } + + $filePath = $this->FilesService->GetFilePath($args['group'], $fileName); + if (file_exists($filePath)) + { + unlink($filePath); + } + + return $this->ApiResponse(array('success' => true)); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index d50fd8f2..59ee8cb4 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -431,9 +431,58 @@ } } }, - "/files/upload/{group}": { - "post": { - "description": "Uploads a single file to /data/storage/{group}/{file_name}", + "/file/{group}": { + "get": { + "description": "Serves the given file (with proper Content-Type header)", + "tags": [ + "Files" + ], + "parameters": [ + { + "in": "path", + "name": "group", + "required": true, + "description": "The file group", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "file_name", + "required": true, + "description": "The file name (including extension)", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The binary file contents (Content-Type header is automatically set based on the file type)", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + }, + "put": { + "description": "Uploads a single file to /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)", "tags": [ "Files" ], @@ -489,11 +538,9 @@ } } } - } - }, - "/files/get/{group}": { - "get": { - "description": "Serves the given file", + }, + "delete": { + "description": "Deletes the given file", "tags": [ "Files" ], @@ -519,12 +566,11 @@ ], "responses": { "200": { - "description": "The binary file contents (mime type is set based on file extension)", + "description": "A VoidApiActionResponse object", "content": { - "application/octet-stream": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/VoidApiActionResponse" } } } @@ -1683,6 +1729,9 @@ "minimum": 0, "default": 0 }, + "picture_file_name": { + "type": "string" + }, "row_created_timestamp": { "type": "string", "format": "date-time" diff --git a/helpers/extensions.php b/helpers/extensions.php index 3c17414f..06e11b85 100644 --- a/helpers/extensions.php +++ b/helpers/extensions.php @@ -192,7 +192,7 @@ function Pluralize($number, $singularForm, $pluralForm) function IsValidFileName($fileName) { - if(preg_match('#^[a-z0-9]+\.[a-z]+?$#i', $fileName)) + if(preg_match('=^[^/?*;:{}\\\\]+\.[^/?*;:{}\\\\]+$=', $fileName)) { return true; } diff --git a/public/js/extensions.js b/public/js/extensions.js index 693d3acd..16419e21 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -54,3 +54,13 @@ BoolVal = function(test) return false; } } + +GetFileNameFromPath = function(path) +{ + return path.split("/").pop().split("\\").pop(); +} + +GetFileExtension = function(pathOrFileName) +{ + return pathOrFileName.split(".").pop(); +} diff --git a/public/js/grocy.js b/public/js/grocy.js index 3b21527d..f0257cf5 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -177,15 +177,10 @@ Grocy.Api.Post = function(apiFunction, jsonData, success, error) xhr.send(JSON.stringify(jsonData)); }; -Grocy.Api.UploadFile = function(fileInput, group, success, error) +Grocy.Api.UploadFile = function(file, group, fileName, success, error) { - if (fileInput[0].files.length === 0) - { - return; - } - var xhr = new XMLHttpRequest(); - var url = U('/api/files/upload/' + group + '?file_name=' + encodeURIComponent(fileInput[0].files[0].name)); + var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName)); xhr.onreadystatechange = function() { @@ -208,9 +203,40 @@ Grocy.Api.UploadFile = function(fileInput, group, success, error) } }; - xhr.open('POST', url, true); + xhr.open('PUT', url, true); xhr.setRequestHeader('Content-type', 'application/octet-stream'); - xhr.send(fileInput[0].files[0]); + xhr.send(file); +}; + +Grocy.Api.DeleteFile = function(fileName, group, success, error) +{ + var xhr = new XMLHttpRequest(); + var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName)); + + xhr.onreadystatechange = function() + { + if (xhr.readyState === XMLHttpRequest.DONE) + { + if (xhr.status === 200) + { + if (success) + { + success(JSON.parse(xhr.responseText)); + } + } + else + { + if (error) + { + error(xhr); + } + } + } + }; + + xhr.open('DELETE', url, true); + xhr.setRequestHeader('Content-type', 'application/json'); + xhr.send(); }; Grocy.FrontendHelpers = { }; @@ -284,3 +310,15 @@ $(".user-setting-control").on("change", function() } ); }); + +// Show file name Bootstrap custom file input +$('input.custom-file-input').on('change', function() +{ + $(this).next('.custom-file-label').html(GetFileNameFromPath($(this).val())); +}); + +// Translation of "Browse"-button of Bootstrap custom file input +if ($(".custom-file-label").length > 0) +{ + $(" +@endpush + @section('content')
@@ -74,14 +82,12 @@ data-consume-amount="{{ $currentStockEntry->amount }}"> {{ $L('All') }} - - - - - {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} + + {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) @endif {{ $currentStockEntry->amount }} {{ Pluralize($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }} From 9dd57decdfb247c60e69960fe5455b6a005a7e34 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 2 Oct 2018 16:48:39 +0200 Subject: [PATCH 22/46] Finish "pictures for products" features (now closes #58) --- localization/de.php | 8 ++++++++ public/viewjs/stockoverview.js | 25 +++++++++++++++++++++++-- services/DemoDataGeneratorService.php | 26 +++++++++++++++++++++----- views/productform.blade.php | 2 +- views/stockoverview.blade.php | 1 + 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/localization/de.php b/localization/de.php index 9cbbe54b..60503791 100644 --- a/localization/de.php +++ b/localization/de.php @@ -257,6 +257,14 @@ return array( 'in format' => 'im Format', 'To' => 'Bis', 'Time range goes over midnight' => 'Zeitraum geht über Mitternacht', + 'Product picture' => 'Produktbild', + 'No file selected' => 'Keine Datei ausgewählt', + 'If you don\'t select a file, the current picture will not be altered' => 'Wenn du keine Datei auswählst, wird das aktuelle Bild nicht verändert', + 'Current picture' => 'Aktuelles Bild', + 'Delete' => 'Löschen', + 'The current picture will be deleted when you save the product' => 'Das aktuelle Bild wird beim Speichern des Produkts gelöscht', + 'Select file' => 'Datei auswählen', + 'Image of product #1' => 'Bild des Produkts #1', //Constants 'manually' => 'Manuell', diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index ad645461..bd0fa982 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -149,10 +149,31 @@ $(document).on("click", ".product-name-cell", function(e) { var pictureUrl = $(e.currentTarget).attr("data-picture-url"); var productName = $(e.currentTarget).attr("data-product-name"); + var productId = $(e.currentTarget).attr("data-product-id"); - bootbox.alert({ + bootbox.dialog({ title: L("Image of product #1", productName), - message: "" + message: "", + backdrop: false, + onEscape: true, + buttons: { + editproduct: { + label: ' ' + L('Edit product'), + className: 'btn-info responsive-button', + callback: function () + { + window.location.href = U('/product/' + productId + '?returnto=' + encodeURIComponent(window.location.pathname) + '#product-picture'); + } + }, + close: { + label: L('Close'), + className: 'btn-default responsive-button', + callback: function() + { + bootbox.hideAll(); + } + } + } }); } }); diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 718affea..6bb97d06 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -39,9 +39,9 @@ class DemoDataGeneratorService extends BaseService INSERT INTO product_groups(name) VALUES ('06 {$localizationService->Localize('Refrigerated products')}'); --6 DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here... - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8, 1); --1 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8, 1, 'cookies.jpg'); --1 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Chocolate')}', 3, 3, 3, 1, 8, 1); --2 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8, 1); --3 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8, 1, 'gummybears.jpg'); --3 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Crisps')}', 3, 3, 3, 1, 10, 1); --4 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Eggs')}', 2, 3, 2, 10, 5); --5 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Noodles')}', 3, 3, 3, 1, 6); --6 @@ -50,10 +50,10 @@ class DemoDataGeneratorService extends BaseService INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Yogurt')}', 2, 6, 6, 1, 6); --9 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cheese')}', 2, 3, 3, 1, 6); --10 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cold cuts')}', 2, 3, 3, 1, 6); --11 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1, 5); --12 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1, 5); --13 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1, 5, 'paprika.jpg'); --12 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1, 5, 'cucumber.jpg'); --13 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Radish')}', 2, 7, 7, 1, 5); --14 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1, 5); --15 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1, 5, 'tomato.jpg'); --15 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Pizza dough')}', 3, 3, 3, 1, 6); --16 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Sieved tomatoes')}', 4, 5, 5, 1, 3); --17 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Salami')}', 2, 3, 3, 1, 6); --18 @@ -201,6 +201,22 @@ class DemoDataGeneratorService extends BaseService $batteriesService->TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-50 days'))); $batteriesService->TrackChargeCycle(3, date('Y-m-d H:i:s', strtotime('-65 days'))); $batteriesService->TrackChargeCycle(4, date('Y-m-d H:i:s', strtotime('-56 days'))); + + // Download demo product pictures + $productPicturesFolder = GROCY_DATAPATH . '/storage/productpictures'; + mkdir(GROCY_DATAPATH . '/storage'); + mkdir(GROCY_DATAPATH . '/storage/productpictures'); + $sslOptions = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + ), + ); + file_put_contents("$productPicturesFolder/cookies.jpg", file_get_contents('https://releases.grocy.info/demoresources/cookies.jpg', false, stream_context_create($sslOptions))); + file_put_contents("$productPicturesFolder/cucumber.jpg", file_get_contents('https://releases.grocy.info/demoresources/cucumber.jpg', false, stream_context_create($sslOptions))); + file_put_contents("$productPicturesFolder/gummybears.jpg", file_get_contents('https://releases.grocy.info/demoresources/gummybears.jpg', false, stream_context_create($sslOptions))); + file_put_contents("$productPicturesFolder/paprika.jpg", file_get_contents('https://releases.grocy.info/demoresources/paprika.jpg', false, stream_context_create($sslOptions))); + file_put_contents("$productPicturesFolder/tomato.jpg", file_get_contents('https://releases.grocy.info/demoresources/tomato.jpg', false, stream_context_create($sslOptions))); } } diff --git a/views/productform.blade.php b/views/productform.blade.php index 0fd69786..f39e16c3 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -130,7 +130,7 @@ @if(!empty($product->picture_file_name)) - +

{{ $L('The current picture will be deleted when you save the product') }}

@else

{{ $L('No picture') }}

diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 35be88c6..5e33d6c4 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -85,6 +85,7 @@ {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) @endif From bb9caf9cc9f7a78aafb14de61724dfb7342e4ef6 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 2 Oct 2018 17:06:21 +0200 Subject: [PATCH 23/46] Fixed volatil stock logic (fixes #69) --- controllers/StockApiController.php | 2 +- services/StockService.php | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 350a580f..04f9e04f 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -126,7 +126,7 @@ class StockApiController extends BaseApiController $nextXDays = $request->getQueryParams()['expiring_days']; } - $expiringProducts = $this->StockService->GetExpiringProducts($nextXDays); + $expiringProducts = $this->StockService->GetExpiringProducts($nextXDays, true); $expiredProducts = $this->StockService->GetExpiringProducts(-1); $missingProducts = $this->StockService->GetMissingProducts(); return $this->ApiResponse(array( diff --git a/services/StockService.php b/services/StockService.php index c6f81c90..9cf0c95d 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -8,9 +8,14 @@ class StockService extends BaseService const TRANSACTION_TYPE_CONSUME = 'consume'; const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction'; - public function GetCurrentStock() + public function GetCurrentStock($includeNotInStockButMissingProducts = false) { $sql = 'SELECT * from stock_current'; + if ($includeNotInStockButMissingProducts) + { + $sql = 'SELECT * from stock_current WHERE best_before_date IS NOT NULL'; + } + return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } @@ -20,10 +25,17 @@ class StockService extends BaseService return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } - public function GetExpiringProducts(int $days = 5) + public function GetExpiringProducts(int $days = 5, bool $excludeExpired = false) { - $currentStock = $this->GetCurrentStock(); - return FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime("+$days days")), '<'); + $currentStock = $this->GetCurrentStock(true); + $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime("+$days days")), '<'); + + if ($excludeExpired) + { + $currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('now')), '>'); + } + + return $currentStock; } public function GetProductDetails(int $productId) From ae58606d04f90be687b00ee1fb23fe3fc71ac116 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 2 Oct 2018 18:00:52 +0200 Subject: [PATCH 24/46] Center title in product picture dialog --- public/css/grocy.css | 6 ++++++ public/viewjs/stockoverview.js | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/css/grocy.css b/public/css/grocy.css index 93802c5e..179db199 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -81,6 +81,12 @@ input::-webkit-inner-spin-button { -webkit-appearance: none; } +.centered-dialog .modal-title, +.centered-dialog .modal-body { + margin-left: auto; + margin-right: auto; +} + /* Navigation style customizations */ #mainNav { background-color: #e5e5e5 !important; diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index bd0fa982..f5e6b742 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -153,9 +153,11 @@ $(document).on("click", ".product-name-cell", function(e) bootbox.dialog({ title: L("Image of product #1", productName), - message: "", + message: "", backdrop: false, onEscape: true, + closeButton: false, + className: 'centered-dialog', buttons: { editproduct: { label: ' ' + L('Edit product'), From 6090ac621efc24c96705be8230a293f4d4d55c23 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 2 Oct 2018 18:17:26 +0200 Subject: [PATCH 25/46] Prevent deletion of products with current stock (closes #81) --- localization/de.php | 2 ++ public/viewjs/products.js | 71 +++++++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/localization/de.php b/localization/de.php index 60503791..50e8d46c 100644 --- a/localization/de.php +++ b/localization/de.php @@ -265,6 +265,8 @@ return array( 'The current picture will be deleted when you save the product' => 'Das aktuelle Bild wird beim Speichern des Produkts gelöscht', 'Select file' => 'Datei auswählen', 'Image of product #1' => 'Bild des Produkts #1', + 'This product cannot be deleted because it is in stock, please remove the stock amount first.' => 'Dieses Produkt kann nicht gelöscht werden, da es auf Lager ist, bitte zuerst den Bestand entfernen.', + 'Delete not possible' => 'Löschen nicht möglich', //Constants 'manually' => 'Manuell', diff --git a/public/viewjs/products.js b/public/viewjs/products.js index 79aec640..838c725d 100644 --- a/public/viewjs/products.js +++ b/public/viewjs/products.js @@ -35,33 +35,54 @@ $(document).on('click', '.product-delete-button', function (e) var objectName = $(e.currentTarget).attr('data-product-name'); var objectId = $(e.currentTarget).attr('data-product-id'); - bootbox.confirm({ - message: L('Are you sure to delete product "#1"?', objectName), - buttons: { - confirm: { - label: L('Yes'), - className: 'btn-success' - }, - cancel: { - label: L('No'), - className: 'btn-danger' + Grocy.Api.Get('stock/get-product-details/' + objectId, + function(productDetails) + { + var stockAmount = productDetails.stock_amount || '0'; + + if (stockAmount.toString() == "0") + { + bootbox.confirm({ + message: L('Are you sure to delete product "#1"?', objectName), + buttons: { + confirm: { + label: L('Yes'), + className: 'btn-success' + }, + cancel: { + label: L('No'), + className: 'btn-danger' + } + }, + callback: function (result) + { + if (result === true) + { + Grocy.Api.Get('delete-object/products/' + objectId, + function (result) + { + window.location.href = U('/products'); + }, + function (xhr) + { + console.error(xhr); + } + ); + } + } + }); + } + else + { + bootbox.alert({ + title: L('Delete not possible'), + message: L('This product cannot be deleted because it is in stock, please remove the stock amount first.') + '

' + L('Stock amount') + ': ' + stockAmount + ' ' + Pluralize(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural) + }); } }, - callback: function(result) + function(xhr) { - if (result === true) - { - Grocy.Api.Get('delete-object/products/' + objectId, - function(result) - { - window.location.href = U('/products'); - }, - function(xhr) - { - console.error(xhr); - } - ); - } + console.error(xhr); } - }); + ); }); From f90faca62ee0a3b346cb1b7f51d8722971313edf Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 2 Oct 2018 18:33:16 +0200 Subject: [PATCH 26/46] Accept only files for product picture file input --- views/productform.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/productform.blade.php b/views/productform.blade.php index f39e16c3..0ebcc848 100644 --- a/views/productform.blade.php +++ b/views/productform.blade.php @@ -116,7 +116,7 @@
- +

{{ $L('If you don\'t select a file, the current picture will not be altered') }}

From edb986ce249e10371c95e8a1a33f26a50e9c04f1 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Tue, 2 Oct 2018 20:03:08 +0200 Subject: [PATCH 27/46] Added a quick mockup for equipment / instruction manuals (references #25) --- controllers/EquipmentController.php | 31 +++++++ grocy.openapi.json | 3 +- migrations/0041.sql | 7 ++ public/viewjs/equipment.js | 81 +++++++++++++++++ public/viewjs/equipmentform.js | 123 ++++++++++++++++++++++++++ routes.php | 4 + services/DemoDataGeneratorService.php | 3 + views/equipment.blade.php | 62 +++++++++++++ views/equipmentform.blade.php | 65 ++++++++++++++ views/layout/default.blade.php | 6 ++ views/productform.blade.php | 2 +- 11 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 controllers/EquipmentController.php create mode 100644 migrations/0041.sql create mode 100644 public/viewjs/equipment.js create mode 100644 public/viewjs/equipmentform.js create mode 100644 views/equipment.blade.php create mode 100644 views/equipmentform.blade.php diff --git a/controllers/EquipmentController.php b/controllers/EquipmentController.php new file mode 100644 index 00000000..a5804a5a --- /dev/null +++ b/controllers/EquipmentController.php @@ -0,0 +1,31 @@ +AppContainer->view->render($response, 'equipment', [ + 'equipment' => $this->Database->equipment()->orderBy('name') + ]); + } + + public function EditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + if ($args['equipmentId'] == 'new') + { + return $this->AppContainer->view->render($response, 'equipmentform', [ + 'mode' => 'create' + ]); + } + else + { + return $this->AppContainer->view->render($response, 'equipmentform', [ + 'equipment' => $this->Database->equipment($args['equipmentId']), + 'mode' => 'edit' + ]); + } + } +} diff --git a/grocy.openapi.json b/grocy.openapi.json index 59ee8cb4..4ab5c640 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1677,7 +1677,8 @@ "recipes_pos", "tasks", "task_categories", - "product_groups" + "product_groups", + "equipment" ] }, "StockTransactionType": { diff --git a/migrations/0041.sql b/migrations/0041.sql new file mode 100644 index 00000000..78f3afec --- /dev/null +++ b/migrations/0041.sql @@ -0,0 +1,7 @@ +CREATE TABLE equipment ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL UNIQUE, + description TEXT, + instruction_manual_file_name TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +) diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js new file mode 100644 index 00000000..cfdec472 --- /dev/null +++ b/public/viewjs/equipment.js @@ -0,0 +1,81 @@ +var equipmentTable = $('#equipment-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 = ""; + }); + }, + 'select': 'single', + 'initComplete': function() + { + this.api().row({ order: 'current' }, 0).select(); + } +}); + +equipmentTable.on('select', function(e, dt, type, indexes) +{ + if (type === 'row') + { + var selectedEquipmentId = $(equipmentTable.row(indexes[0]).node()).data("equipment-id"); + console.log(selectedEquipmentId); + } +}); + +$("#search").on("keyup", function() +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + equipmentTable.search(value).draw(); +}); + +$(document).on('click', '.equipment-delete-button', function (e) +{ + var objectName = $(e.currentTarget).attr('data-equipment-name'); + var objectId = $(e.currentTarget).attr('data-equipment-id'); + + bootbox.confirm({ + message: L('Are you sure to delete equipment "#1"?', objectName), + buttons: { + confirm: { + label: L('Yes'), + className: 'btn-success' + }, + cancel: { + label: L('No'), + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.Api.Get('delete-object/equipment/' + objectId, + function(result) + { + window.location.href = U('/equipment'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/equipmentform.js b/public/viewjs/equipmentform.js new file mode 100644 index 00000000..65cf405b --- /dev/null +++ b/public/viewjs/equipmentform.js @@ -0,0 +1,123 @@ +$('#save-equipment-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = $('#equipment-form').serializeJSON(); + if ($("#instruction-manual")[0].files.length > 0) + { + var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100); + jsonData.instruction_manual_file_name = someRandomStuff + $("#instruction-manual")[0].files[0].name; + } + + if (Grocy.DeleteInstructionManualOnSave) + { + jsonData.instruction_manual_file_name = null; + } + + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('add-object/equipment', jsonData, + function(result) + { + if (jsonData.hasOwnProperty("instruction_manual_file_name") && !Grocy.DeleteInstructionManualOnSave) + { + Grocy.Api.UploadFile($("#instruction-manual")[0].files[0], 'equipmentmanuals', jsonData.instruction_manual_file_name, + function(result) + { + window.location.href = U('/equipment'); + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + window.location.href = U('/equipment'); + } + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + if (Grocy.DeleteInstructionManualOnSave) + { + Grocy.Api.DeleteFile(Grocy.InstructionManualFileNameName, 'equipmentmanuals', + function(result) + { + // Nothing to do + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + }; + + Grocy.Api.Post('edit-object/equipment/' + Grocy.EditObjectId, jsonData, + function(result) + { + if (jsonData.hasOwnProperty("instruction_manual_file_name") && !Grocy.DeleteInstructionManualOnSave) + { + Grocy.Api.UploadFile($("#instruction-manual")[0].files[0], 'equipmentmanuals', jsonData.instruction_manual_file_name, + function(result) + { + window.location.href = U('/equipment');; + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } + else + { + window.location.href = U('/equipment');; + } + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); + } +}); + +$('#equipment-form input').keyup(function(event) +{ + Grocy.FrontendHelpers.ValidateForm('equipment-form'); +}); + +$('#equipment-form input').keydown(function(event) +{ + if (event.keyCode === 13) //Enter + { + event.preventDefault(); + + if (document.getElementById('equipment-form').checkValidity() === false) //There is at least one validation error + { + return false; + } + else + { + $('#save-equipment-button').click(); + } + } +}); + +Grocy.DeleteInstructionManualOnSave = false; +$('#delete-current-instruction-manual-button').on('click', function (e) +{ + Grocy.DeleteInstructionManualOnSave = true; + //$("#current-instruction-manual").addClass("d-none"); + $("#delete-current-instruction-manual-on-save-hint").removeClass("d-none"); + $("#delete-current-instruction-manual-button").addClass("disabled"); +}); + +$('#name').focus(); +Grocy.FrontendHelpers.ValidateForm('equipment-form'); diff --git a/routes.php b/routes.php index b34fd8a8..4b3cbce7 100644 --- a/routes.php +++ b/routes.php @@ -61,6 +61,10 @@ $app->group('', function() $this->get('/taskcategories', '\Grocy\Controllers\TasksController:TaskCategoriesList'); $this->get('/taskcategory/{categoryId}', '\Grocy\Controllers\TasksController:TaskCategoryEditForm'); + // Equipment routes + $this->get('/equipment', '\Grocy\Controllers\EquipmentController:Overview'); + $this->get('/equipment/{equipmentId}', '\Grocy\Controllers\EquipmentController:EditForm'); + // OpenAPI routes $this->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi'); $this->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList'); diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 6bb97d06..ac144179 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -105,6 +105,9 @@ class DemoDataGeneratorService extends BaseService INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Find a solution for what to do when I forget the door keys')}', date(datetime('now', 'localtime'), '+3 day'), 1); INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}3', date(datetime('now', 'localtime'), '+4 day'), 1); + INSERT INTO equipment (name, description) VALUES ('{$localizationService->Localize('Coffee machine')}', '{$loremIpsum}'); --1 + INSERT INTO equipment (name, description) VALUES ('{$localizationService->Localize('Dishwasher')}', '{$loremIpsum}'); --2 + INSERT INTO migrations (migration) VALUES (-1); "; diff --git a/views/equipment.blade.php b/views/equipment.blade.php new file mode 100644 index 00000000..f00ec448 --- /dev/null +++ b/views/equipment.blade.php @@ -0,0 +1,62 @@ +@extends('layout.default') + +@section('title', $L('Equipment')) +@section('activeNav', 'equipment') +@section('viewJsName', 'equipment') + +@section('content') +
+
+

+ @yield('title') + +  {{ $L('Add') }} + +

+
+
+ +
+
+ + +
+
+ +
+ +
+ + + + + + + + + @foreach($equipment as $equipmentItem) + + + + + @endforeach + +
#{{ $L('Name') }}
+ + + + + + + + {{ $equipmentItem->name }} +
+
+ +
+

{{ $L('Instruction manual') }}

+

{{ $L('The selected equipment has no instruction manual') }}

+

TODO: Here the current instruction manual needs to be shown (PDF.js), if any...

+
+
+@stop diff --git a/views/equipmentform.blade.php b/views/equipmentform.blade.php new file mode 100644 index 00000000..5b290cb8 --- /dev/null +++ b/views/equipmentform.blade.php @@ -0,0 +1,65 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $L('Edit equipment')) +@else + @section('title', $L('Create equipment')) +@endif + +@section('viewJsName', 'equipmentform') + +@section('content') +
+ +
+

@yield('title')

+ + + + @if($mode == 'edit') + + + @if(!empty($equipment->instruction_manual_file_name)) + + @endif + @endif + +
+ +
+ + +
{{ $L('A name is required') }}
+
+ +
+ +
+ + +
+

{{ $L('If you don\'t select a file, the current instruction manual will not be altered') }}

+
+ +
+ + +
+ + + +
+
+ +
+ + + @if(!empty($equipment->instruction_manual_file_name)) +

TODO: Here the current instruction manual needs to be shown (PDF.js), if any...

+

{{ $L('The current instruction manual will be deleted when you save the equipment') }}

+ @else +

{{ $L('No instruction manual') }}

+ @endif +
+
+@stop diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 5cd74855..5d6490c2 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -93,6 +93,12 @@ {{ $L('Batteries overview') }} +
diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 5d6490c2..ff211688 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -95,7 +95,7 @@ diff --git a/yarn.lock b/yarn.lock index 02957e0a..8b4aa46e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ version "5.3.1" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.3.1.tgz#5466b8f31c1f493a96754c1426c25796d0633dd9" -"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2": +"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2", "tagmanager@https://github.com/max-favilli/tagmanager.git#3.0.2": version "3.0.1" resolved "https://github.com/max-favilli/tagmanager.git#df9eb9935c8585a392dfc00602f890caf233fa94" dependencies: @@ -202,6 +202,10 @@ startbootstrap-sb-admin@^4.0.0: jquery "3.3.1" jquery.easing "^1.4.1" +summernote@^0.8.10: + version "0.8.10" + resolved "https://registry.yarnpkg.com/summernote/-/summernote-0.8.10.tgz#21a5d7f18a3b07500b58b60d5907417a54897520" + swagger-ui-dist@^3.17.3: version "3.19.1" resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.1.tgz#ee176cfb070470bb81ba98a686e808035a882cb1" From ebd9b1b8515acc14e58f1f466e4a3c57908bdbdb Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 3 Oct 2018 16:40:40 +0200 Subject: [PATCH 31/46] Add possibility to show equipment notes/instruction manuals also in fullscreen mode (references #25) --- public/viewjs/equipment.js | 17 ++++++++++++++++- views/equipment.blade.php | 28 ++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js index 85c0d8ed..d6599309 100644 --- a/public/viewjs/equipment.js +++ b/public/viewjs/equipment.js @@ -39,7 +39,8 @@ function DisplayEquipment(id) Grocy.Api.Get('get-object/equipment/' + id, function(equipmentItem) { - $("#description-tab").html(equipmentItem.description); + $(".selected-equipment-name").text(equipmentItem.name); + $("#description-tab-content").html(equipmentItem.description); if (equipmentItem.instruction_manual_file_name !== null && !equipmentItem.instruction_manual_file_name.isEmpty()) { @@ -111,3 +112,17 @@ $(document).on('click', '.equipment-delete-button', function (e) } }); }); + +$("#selectedEquipmentInstructionManualToggleFullscreenButton").on('click', function(e) +{ + $("#selectedEquipmentInstructionManualCard").toggleClass("fullscreen"); + $("#selectedEquipmentInstructionManualCard .card-header").toggleClass("fixed-top"); + $("#selectedEquipmentInstructionManualCard .card-body").toggleClass("mt-5"); +}); + +$("#selectedEquipmentDescriptionToggleFullscreenButton").on('click', function(e) +{ + $("#selectedEquipmentDescriptionCard").toggleClass("fullscreen"); + $("#selectedEquipmentDescriptionCard .card-header").toggleClass("fixed-top"); + $("#selectedEquipmentDescriptionCard .card-body").toggleClass("mt-5"); +}); diff --git a/views/equipment.blade.php b/views/equipment.blade.php index 08952e65..c5d56386 100644 --- a/views/equipment.blade.php +++ b/views/equipment.blade.php @@ -62,13 +62,33 @@ {{ $L('Notes') }} -
+
-

{{ $L('The selected equipment has no instruction manual') }}

- +
+
+ + + + +
+
+

{{ $L('The selected equipment has no instruction manual') }}

+ +
+
- +
+
+ + + + +
+
+
+
+
From ebd24bf30e4a6f9ad94af9c5b00ab457d2a83303 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 3 Oct 2018 16:41:21 +0200 Subject: [PATCH 32/46] Use new editor also for recipes --- public/viewjs/recipeform.js | 5 +++++ public/viewjs/recipes.js | 1 + services/DemoDataGeneratorService.php | 8 ++++---- views/recipeform.blade.php | 11 ++++++++++- views/recipes.blade.php | 2 +- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js index 0d42139f..37e53075 100644 --- a/public/viewjs/recipeform.js +++ b/public/viewjs/recipeform.js @@ -169,3 +169,8 @@ $("#recipe-pos-add-button").on("click", function(e) } ); }); + +$('#description').summernote({ + minHeight: '300px', + lang: L('summernote_locale') +}); diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index cb11ad21..44308ddd 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -159,4 +159,5 @@ $("#selectedRecipeToggleFullscreenButton").on('click', function(e) { $("#selectedRecipeCard").toggleClass("fullscreen"); $("#selectedRecipeCard .card-header").toggleClass("fixed-top"); + $("#selectedRecipeCard .card-body").toggleClass("mt-5"); }); diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index 75c3c483..15e5f703 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -67,10 +67,10 @@ class DemoDataGeneratorService extends BaseService INSERT INTO shopping_list (product_id, amount) VALUES (20, 1); INSERT INTO shopping_list (product_id, amount) VALUES (17, 1); - INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsum}'); --1 - INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsum}'); --2 - INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsum}'); --3 - INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsum}'); --4 + INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsumWithHtmlFormattings}'); --1 + INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsumWithHtmlFormattings}'); --2 + INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsumWithHtmlFormattings}'); --3 + INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsumWithHtmlFormattings}'); --4 INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 16, 1); INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 17, 1); diff --git a/views/recipeform.blade.php b/views/recipeform.blade.php index 0dbdc3b7..feddb61e 100644 --- a/views/recipeform.blade.php +++ b/views/recipeform.blade.php @@ -8,6 +8,15 @@ @section('viewJsName', 'recipeform') +@push('pageScripts') + + @if(!empty($L('summernote_locale')))@endif +@endpush + +@push('pageStyles') + +@endpush + @section('content')
@@ -33,7 +42,7 @@
- +
diff --git a/views/recipes.blade.php b/views/recipes.blade.php index 1158519a..4fa06428 100644 --- a/views/recipes.blade.php +++ b/views/recipes.blade.php @@ -82,7 +82,7 @@
{{ $L('Preparation') }}
- {!! nl2br(htmlentities($selectedRecipe->description)) !!} + {!! $selectedRecipe->description !!}
From 3b10906e780d89dc39f10f0f202b12e20b6a7d13 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 3 Oct 2018 18:04:46 +0200 Subject: [PATCH 33/46] Optimize space around the embedded PDF a little bit (references #25) --- views/equipment.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/equipment.blade.php b/views/equipment.blade.php index c5d56386..5079f48e 100644 --- a/views/equipment.blade.php +++ b/views/equipment.blade.php @@ -71,7 +71,7 @@
-
+

{{ $L('The selected equipment has no instruction manual') }}

From 7ba6fc875be579b793e80f88caf1472e42ce49be Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Wed, 3 Oct 2018 19:05:00 +0200 Subject: [PATCH 34/46] Improve responsive embeds (references #25) --- public/js/grocy.js | 19 +++++++++++++++++++ public/viewjs/equipment.js | 3 +++ public/viewjs/equipmentform.js | 2 ++ views/equipment.blade.php | 16 ++++------------ views/equipmentform.blade.php | 2 +- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/public/js/grocy.js b/public/js/grocy.js index f0257cf5..2f6c171d 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -322,3 +322,22 @@ if ($(".custom-file-label").length > 0) { $("