diff --git a/changelog/66_UNRELEASED_2022-02-11.md b/changelog/66_UNRELEASED_2022-02-11.md index 0dd9a9f3..4fd07ee7 100644 --- a/changelog/66_UNRELEASED_2022-02-11.md +++ b/changelog/66_UNRELEASED_2022-02-11.md @@ -17,6 +17,7 @@ - Background: Before v3.0.0 recipe costs were only based on the last price per product and since v3.0.0 the "real costs" (based on the default consume rule "Opened first, then first due first, then first in first out") are used, means out of stock items have no price - so using the last price for out of stock items should reflect the current real costs better - Added a new recipes setting (top right corner settings menu) "Show the recipe list and the recipe side by side" (defaults to enabled, so no changed behaviour when not configured) - When disabled, on the recipes page, the recipe list is displayed full-width and the recipe will be shown in a popup instead of on the right side +- Recipes are now also grocycode enabled (works like any other grocycode; download/print it via the recipes edit page or the more/context menu on the recipes page; use/scan it at any place a recipe can be selected) - Performance improvements (page loading time) of the recipes page - Fixed that when adding missing recipe ingredients, with the option "Only check if any amount is in stock" enabled, to the shopping list, unit conversions (if any) weren't considered - Fixed that the recipe stock fulfillment information about shopping list amounts was not correct when the ingredient had a decimal amount @@ -73,4 +74,5 @@ - The API endpoint `/stock/shoppinglist/clear` has now a new optional request body parameter `done_only` (to only remove done items from the given shopping list, defaults to `false`) - The API endpoint `/chores/{choreId}/execute` has now a new optional request body parameter `skipped` (to skip the next chore schedule, defaults to `false`) - The API endpoint `/chores/{choreId}` has new response field/property `average_execution_frequency_hours` (contains the average past execution frequency in hours or `null`, when the chore was never executed before) +- New API endpoint `/recipes/{recipeId}/printlabel` (to print recipe grocycodes on the configured label printer) - Fixed that the barcode lookup for the "Stock by-barcode" API endpoints was case sensitive diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index 263d3998..c8a5f433 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -3,6 +3,8 @@ namespace Grocy\Controllers; use Grocy\Controllers\Users\User; +use Grocy\Helpers\WebhookRunner; +use Grocy\Helpers\Grocycode; class RecipesApiController extends BaseApiController { @@ -76,4 +78,28 @@ class RecipesApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function RecipePrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + try + { + $recipe = $this->getDatabase()->recipes()->where('id', $args['recipeId'])->fetch(); + + $webhookData = array_merge([ + 'recipe' => $recipe->name, + 'grocycode' => (string)(new Grocycode(Grocycode::RECIPE, $args['recipeId'])), + ], GROCY_LABEL_PRINTER_PARAMS); + + if (GROCY_LABEL_PRINTER_RUN_SERVER) + { + (new WebhookRunner())->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON); + } + + return $this->ApiResponse($response, $webhookData); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/controllers/RecipesController.php b/controllers/RecipesController.php index 39672f0e..8c6df6c1 100644 --- a/controllers/RecipesController.php +++ b/controllers/RecipesController.php @@ -3,9 +3,12 @@ namespace Grocy\Controllers; use Grocy\Services\RecipesService; +use Grocy\Helpers\Grocycode; class RecipesController extends BaseController { + use GrocycodeTrait; + public function MealPlan(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { $start = date('Y-m-d'); @@ -213,4 +216,10 @@ class RecipesController extends BaseController 'mealplanSections' => $this->getDatabase()->meal_plan_sections()->where('id > 0')->orderBy('sort_number') ]); } + + public function RecipeGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + $gc = new Grocycode(Grocycode::RECIPE, $args['recipeId']); + return $this->ServeGrocycodeImage($request, $response, $gc); + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index 4fd689b5..6890654d 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -3496,6 +3496,48 @@ } } }, + "/recipes/{recipeId}/printlabel": { + "get": { + "summary": "Prints the grocycode label of the given recipe on the configured label printer", + "tags": [ + "Recipes" + ], + "parameters": [ + { + "in": "path", + "name": "recipeId", + "required": true, + "description": "A valid recipe id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "WebHook data" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing recipe, error on WebHook execution)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error400" + } + } + } + } + } + } + }, "/chores": { "get": { "summary": "Returns all chores incl. the next estimated execution time per chore", diff --git a/helpers/Grocycode.php b/helpers/Grocycode.php index bfafeaf5..6f334946 100644 --- a/helpers/Grocycode.php +++ b/helpers/Grocycode.php @@ -23,6 +23,8 @@ class Grocycode public const CHORE = 'c'; + public const RECIPE = 'r'; + public const MAGIC = 'grcy'; /** @@ -55,7 +57,7 @@ class Grocycode /** * An array that registers all valid grocycode types. Register yours here by appending to this array. */ - public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE]; + public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE, self::RECIPE]; private $type; diff --git a/public/viewjs/components/recipepicker.js b/public/viewjs/components/recipepicker.js index 29488701..e7c198c0 100644 --- a/public/viewjs/components/recipepicker.js +++ b/public/viewjs/components/recipepicker.js @@ -37,7 +37,7 @@ Grocy.Components.RecipePicker.Clear = function() $('.recipe-combobox').combobox({ appendId: '_text_input', bsVersion: '4', - clearIfNoMatch: true + clearIfNoMatch: false }); var prefillByName = Grocy.Components.RecipePicker.GetPicker().parent().data('prefill-by-name').toString(); @@ -66,3 +66,38 @@ if (typeof prefillById !== "undefined") var nextInputElement = $(Grocy.Components.RecipePicker.GetPicker().parent().data('next-input-selector').toString()); nextInputElement.focus(); } + +$('#recipe_id_text_input').on('blur', function(e) +{ + if ($('#recipe_id').hasClass("combobox-menu-visible")) + { + return; + } + + var input = $('#recipe_id_text_input').val().toString(); + var possibleOptionElement = []; + + // grocycode handling + if (input.startsWith("grcy")) + { + var gc = input.split(":"); + if (gc[1] == "r") + { + possibleOptionElement = $("#recipe_id option[value=\"" + gc[2] + "\"]").first(); + } + + if (possibleOptionElement.length > 0) + { + $('#recipe_id').val(possibleOptionElement.val()); + $('#recipe_id').data('combobox').refresh(); + $('#recipe_id').trigger('change'); + } + else + { + $('#recipe_id').val(null); + $('#recipe_id_text_input').val(""); + $('#recipe_id').data('combobox').refresh(); + $('#recipe_id').trigger('change'); + } + } +}); diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js index 6237ee9a..775410b9 100644 --- a/public/viewjs/recipeform.js +++ b/public/viewjs/recipeform.js @@ -386,3 +386,18 @@ $(window).on("message", function(e) ); } }); + +$(document).on('click', '.recipe-grocycode-label-print', function(e) +{ + e.preventDefault(); + document.activeElement.blur(); + + var recipeId = $(e.currentTarget).attr('data-recipe-id'); + Grocy.Api.Get('recipes/' + recipeId + '/printlabel', function(labelData) + { + if (Grocy.Webhooks.labelprinter !== undefined) + { + Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData); + } + }); +}); diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index 8d01afb5..a16247dc 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -414,3 +414,18 @@ if (window.location.hash === "#fullscreen") } LoadImagesLazy(); + +$(document).on('click', '.recipe-grocycode-label-print', function(e) +{ + e.preventDefault(); + document.activeElement.blur(); + + var recipeId = $(e.currentTarget).attr('data-recipe-id'); + Grocy.Api.Get('recipes/' + recipeId + '/printlabel', function(labelData) + { + if (Grocy.Webhooks.labelprinter !== undefined) + { + Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData); + } + }); +}); diff --git a/routes.php b/routes.php index e3b201a5..60abf64a 100644 --- a/routes.php +++ b/routes.php @@ -90,6 +90,7 @@ $app->group('', function (RouteCollectorProxy $group) { $group->get('/mealplansections', '\Grocy\Controllers\RecipesController:MealPlanSectionsList'); $group->get('/mealplansection/{sectionId}', '\Grocy\Controllers\RecipesController:MealPlanSectionEditForm'); $group->get('/recipessettings', '\Grocy\Controllers\RecipesController:RecipesSettings'); + $group->get('/recipe/{recipeId}/grocycode', '\Grocy\Controllers\RecipesController:RecipeGrocycodeImage'); } // Chore routes @@ -230,6 +231,8 @@ $app->group('/api', function (RouteCollectorProxy $group) { $group->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe'); $group->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); $group->Post('/recipes/{recipeId}/copy', '\Grocy\Controllers\RecipesApiController:CopyRecipe'); + $group->get('/recipes/{recipeId}/printlabel', '\Grocy\Controllers\RecipesApiController:RecipePrintLabel'); + // Chores $group->get('/chores', '\Grocy\Controllers\ChoresApiController:Current'); diff --git a/views/batteriesoverview.blade.php b/views/batteriesoverview.blade.php index 098aa4f2..82ddebf6 100644 --- a/views/batteriesoverview.blade.php +++ b/views/batteriesoverview.blade.php @@ -150,7 +150,7 @@ {{ $__t('Edit battery') }}
- {!! str_replace('grocycode', 'grocycode', $__t('Download %s grocycode', $__t('Battery'))) !!} diff --git a/views/choresoverview.blade.php b/views/choresoverview.blade.php index 46d222a4..123bb847 100644 --- a/views/choresoverview.blade.php +++ b/views/choresoverview.blade.php @@ -182,7 +182,7 @@ {{ $__t('Edit chore') }} - {!! str_replace('grocycode', 'grocycode', $__t('Download %s grocycode', $__t('Chore'))) !!} diff --git a/views/components/recipepicker.blade.php b/views/components/recipepicker.blade.php index ef70a8ab..419c0bb1 100644 --- a/views/components/recipepicker.blade.php +++ b/views/components/recipepicker.blade.php @@ -14,13 +14,15 @@ data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}"> - + + + {!! str_replace('grocycode', 'grocycode', $__t('Download %s grocycode', $__t('Recipe'))) !!} + + @if(GROCY_FEATURE_FLAG_LABEL_PRINTER) + + {!! str_replace('grocycode', 'grocycode', $__t('Print %s grocycode on label printer', $__t('Recipe'))) !!} + + @endif @@ -281,7 +295,7 @@