Skip to content

Commit

Permalink
Added grocycode for recipes (closes #1562)
Browse files Browse the repository at this point in the history
  • Loading branch information
berrnd committed Feb 11, 2022
1 parent 51fdefa commit 222c518
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 16 deletions.
2 changes: 2 additions & 0 deletions changelog/66_UNRELEASED_2022-02-11.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
26 changes: 26 additions & 0 deletions controllers/RecipesApiController.php
Expand Up @@ -3,6 +3,8 @@
namespace Grocy\Controllers;

use Grocy\Controllers\Users\User;
use Grocy\Helpers\WebhookRunner;
use Grocy\Helpers\Grocycode;

class RecipesApiController extends BaseApiController
{
Expand Down Expand Up @@ -76,4 +78,28 @@ public function CopyRecipe(\Psr\Http\Message\ServerRequestInterface $request, \P
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());
}
}
}
9 changes: 9 additions & 0 deletions controllers/RecipesController.php
Expand Up @@ -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');
Expand Down Expand Up @@ -213,4 +216,10 @@ public function MealPlanSectionsList(\Psr\Http\Message\ServerRequestInterface $r
'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);
}
}
42 changes: 42 additions & 0 deletions grocy.openapi.json
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion helpers/Grocycode.php
Expand Up @@ -23,6 +23,8 @@ class Grocycode

public const CHORE = 'c';

public const RECIPE = 'r';

public const MAGIC = 'grcy';

/**
Expand Down Expand Up @@ -55,7 +57,7 @@ public function __construct(...$args)
/**
* 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;

Expand Down
37 changes: 36 additions & 1 deletion public/viewjs/components/recipepicker.js
Expand Up @@ -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();
Expand Down Expand Up @@ -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');
}
}
});
15 changes: 15 additions & 0 deletions public/viewjs/recipeform.js
Expand Up @@ -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);
}
});
});
15 changes: 15 additions & 0 deletions public/viewjs/recipes.js
Expand Up @@ -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);
}
});
});
3 changes: 3 additions & 0 deletions routes.php
Expand Up @@ -90,6 +90,7 @@
$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
Expand Down Expand Up @@ -230,6 +231,8 @@
$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');
Expand Down
2 changes: 1 addition & 1 deletion views/batteriesoverview.blade.php
Expand Up @@ -150,7 +150,7 @@ class="@if($currentBatteryEntry->due_type == 'overdue') table-danger @elseif($cu
<span class="dropdown-item-text">{{ $__t('Edit battery') }}</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link"
<a class="dropdown-item"
type="button"
href="{{ $U('/battery/' . $currentBatteryEntry->battery_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Battery'))) !!}
Expand Down
2 changes: 1 addition & 1 deletion views/choresoverview.blade.php
Expand Up @@ -182,7 +182,7 @@ class="@if($curentChoreEntry->due_type == 'overdue') table-danger @elseif($curen
<span class="dropdown-item-text">{{ $__t('Edit chore') }}</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link"
<a class="dropdown-item"
type="button"
href="{{ $U('/chore/' . $curentChoreEntry->chore_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Chore'))) !!}
Expand Down
4 changes: 3 additions & 1 deletion views/components/recipepicker.blade.php
Expand Up @@ -14,13 +14,15 @@
data-next-input-selector="{{ $nextInputSelector }}"
data-prefill-by-name="{{ $prefillByName }}"
data-prefill-by-id="{{ $prefillById }}">
<label for="recipe_id">{{ $__t('Recipe') }}
<label class="w-100"
for="recipe_id">{{ $__t('Recipe') }}
@if(!empty($hint))
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $hint }}"></i>
@endif
<i class="fas fa-barcode float-right mt-1"></i>
</label>
<select class="form-control recipe-combobox"
id="recipe_id"
Expand Down
31 changes: 31 additions & 0 deletions views/recipeform.blade.php
Expand Up @@ -336,6 +336,37 @@ class="form-text text-muted font-italic mb-5">{{ $__t('No picture available') }}
@endif
</div>
</div>

<div class="row">
<div class="col">
<div class="title-related-links">
<h4>
<span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode', $__t('Recipe')) }}"></i>
</h4>
<p>
@if($mode == 'edit')
<img src="{{ $U('/recipe/' . $recipe->id . '/grocycode?size=60') }}"
class="float-lg-left">
@endif
</p>
<p>
<a class="btn btn-outline-primary btn-sm"
href="{{ $U('/recipe/' . $recipe->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm recipe-grocycode-label-print"
data-recipe-id="{{ $recipe->id }}"
href="#">
{{ $__t('Print on label printer') }}
</a>
@endif
</p>
</div>
</div>
</div>
</div>

</div>
Expand Down

0 comments on commit 222c518

Please sign in to comment.