From d9246b9b42bec8ca993774459f24d70c8877af14 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 14 Jul 2018 18:23:41 +0200 Subject: [PATCH] Start working on recipes feature --- controllers/BaseController.php | 5 +- controllers/RecipesController.php | 70 ++++++++++++++++ grocy.openapi.json | 4 +- localization/de.php | 8 ++ migrations/0025.sql | 38 +++++++++ public/viewjs/components/productpicker.js | 5 ++ public/viewjs/recipeform.js | 98 +++++++++++++++++++++++ public/viewjs/recipeposform.js | 97 ++++++++++++++++++++++ public/viewjs/recipes.js | 58 ++++++++++++++ routes.php | 7 +- services/RecipesService.php | 18 +++++ views/components/productpicker.blade.php | 3 +- views/layout/default.blade.php | 6 ++ views/recipeform.blade.php | 93 +++++++++++++++++++++ views/recipeposform.blade.php | 55 +++++++++++++ views/recipes.blade.php | 59 ++++++++++++++ yarn.lock | 4 + 17 files changed, 624 insertions(+), 4 deletions(-) create mode 100644 controllers/RecipesController.php create mode 100644 migrations/0025.sql create mode 100644 public/viewjs/recipeform.js create mode 100644 public/viewjs/recipeposform.js create mode 100644 public/viewjs/recipes.js create mode 100644 services/RecipesService.php create mode 100644 views/recipeform.blade.php create mode 100644 views/recipeposform.blade.php create mode 100644 views/recipes.blade.php diff --git a/controllers/BaseController.php b/controllers/BaseController.php index ff45bc06..23eeb04a 100644 --- a/controllers/BaseController.php +++ b/controllers/BaseController.php @@ -11,13 +11,15 @@ class BaseController public function __construct(\Slim\Container $container) { $databaseService = new DatabaseService(); $this->Database = $databaseService->GetDbConnection(); + + $localizationService = new LocalizationService(CULTURE); + $this->LocalizationService = $localizationService; $applicationService = new ApplicationService(); $versionInfo = $applicationService->GetInstalledVersion(); $container->view->set('version', $versionInfo->Version); $container->view->set('releaseDate', $versionInfo->ReleaseDate); - $localizationService = new LocalizationService(CULTURE); $container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations()); $container->view->set('L', function($text, ...$placeholderValues) use($localizationService) { @@ -33,4 +35,5 @@ class BaseController protected $AppContainer; protected $Database; + protected $LocalizationService; } diff --git a/controllers/RecipesController.php b/controllers/RecipesController.php new file mode 100644 index 00000000..49a2813e --- /dev/null +++ b/controllers/RecipesController.php @@ -0,0 +1,70 @@ +RecipesService = new RecipesService(); + } + + protected $RecipesService; + + public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->AppContainer->view->render($response, 'recipes', [ + 'recipes' => $this->Database->recipes()->orderBy('name'), + 'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(), + 'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment() + ]); + } + + public function RecipeEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $recipeId = $args['recipeId']; + if ($recipeId == 'new') + { + $newRecipe = $this->Database->recipes()->createRow(array( + 'name' => $this->LocalizationService->Localize('New recipe') + )); + $newRecipe->save(); + + $recipeId = $this->Database->lastInsertId(); + } + + return $this->AppContainer->view->render($response, 'recipeform', [ + 'recipe' => $this->Database->recipes($recipeId), + 'recipePositions' => $this->Database->recipes_pos()->where('recipe_id', $recipeId), + 'mode' => 'edit', + 'products' => $this->Database->products()->orderBy('name'), + 'quantityunits' => $this->Database->quantity_units()->orderBy('name'), + 'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(), + 'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment() + ]); + } + + public function RecipePosEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + if ($args['recipePosId'] == 'new') + { + return $this->AppContainer->view->render($response, 'recipeposform', [ + 'mode' => 'create', + 'recipe' => $this->Database->recipes($args['recipeId']), + 'products' => $this->Database->products() + ]); + } + else + { + return $this->AppContainer->view->render($response, 'recipeposform', [ + 'mode' => 'edit', + 'recipe' => $this->Database->recipes($args['recipeId']), + 'recipePos' => $this->Database->recipes_pos($args['recipePosId']), + 'products' => $this->Database->products() + ]); + } + } +} diff --git a/grocy.openapi.json b/grocy.openapi.json index a0ea5011..8dc419c6 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -894,7 +894,9 @@ "batteries", "locations", "quantity_units", - "shopping_list" + "shopping_list", + "recipes", + "recipes_pos" ] }, "StockTransactionType": { diff --git a/localization/de.php b/localization/de.php index 52e80882..094306a2 100644 --- a/localization/de.php +++ b/localization/de.php @@ -145,6 +145,14 @@ return array( 'Settings' => 'Einstellungen', 'This can only be before now' => 'Dies kann nur vor jetzt sein', 'Calendar' => 'Kalender', + 'Recipes' => 'Rezepte', + 'Edit recipe' => 'Rezept bearbeiten', + 'New recipe' => 'Neues Rezept', + 'Ingredients list' => 'Zutatenliste', + 'Add recipe ingredient' => 'Rezeptzutat hinzufügen', + 'Edit recipe ingredient' => 'Rezeptzutat bearbeiten', + 'Are you sure to delete recipe "#1"?' => 'Rezept "#1" wirklich löschen?', + 'Are you sure to delete recipe ingredient "#1"?' => 'Rezeptzutat "#1" wirklich löschen?', //Constants 'manually' => 'Manuell', diff --git a/migrations/0025.sql b/migrations/0025.sql new file mode 100644 index 00000000..e128d4f9 --- /dev/null +++ b/migrations/0025.sql @@ -0,0 +1,38 @@ +CREATE TABLE recipes ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL, + description TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +CREATE TABLE recipes_pos ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + recipe_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + amount INTEGER NOT NULL DEFAULT 0, + note TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +CREATE VIEW recipes_fulfillment +AS +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id, + rp.amount AS recipe_amount, + sc.amount AS stock_amount, + CASE WHEN sc.amount >= rp.amount THEN 1 ELSE 0 END AS need_fullfiled +FROM recipes r +LEFT JOIN recipes_pos rp + ON r.id = rp.recipe_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id; + +CREATE VIEW recipes_fulfillment_sum +AS +SELECT + rf.recipe_id, + MIN(rf.need_fullfiled) AS need_fullfiled +FROM recipes_fulfillment rf +GROUP BY rf.recipe_id; diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js index 479b21ef..47f5a449 100644 --- a/public/viewjs/components/productpicker.js +++ b/public/viewjs/components/productpicker.js @@ -49,6 +49,11 @@ $('.combobox').combobox({ }); var prefillProduct = GetUriParam('createdproduct'); +var prefillProduct2 = Grocy.Components.ProductPicker.GetPicker().parent().data('prefill-by-name').toString(); +if (!prefillProduct2.isEmpty()) +{ + prefillProduct = prefillProduct2; +} if (typeof prefillProduct !== "undefined") { var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first(); diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js new file mode 100644 index 00000000..53d82ab1 --- /dev/null +++ b/public/viewjs/recipeform.js @@ -0,0 +1,98 @@ +$('#save-recipe-button').on('click', function(e) +{ + e.preventDefault(); + + Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), + function(result) + { + window.location.href = U('/recipes'); + }, + function(xhr) + { + console.error(xhr); + } + ); +}); + +var recipesPosTables = $('#recipes-pos-table').DataTable({ + 'paginate': false, + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 } + ], + 'language': JSON.parse(L('datatables_localization')), + 'scrollY': false, + 'colReorder': true, + 'stateSave': true +}); + +$("#search").on("keyup", function () +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + recipesPosTables.search(value).draw(); +}); + +Grocy.FrontendHelpers.ValidateForm('recipe-form'); +$("#name").focus(); + +$('#recipe-form input').keyup(function (event) +{ + Grocy.FrontendHelpers.ValidateForm('recipe-form'); +}); + +$('#recipe-form input').keydown(function (event) +{ + if (event.keyCode === 13) //Enter + { + if (document.getElementById('recipe-form').checkValidity() === false) //There is at least one validation error + { + event.preventDefault(); + return false; + } + else + { + $('#save-recipe-button').click(); + } + } +}); + +$(document).on('click', '.recipe-pos-delete-button', function(e) +{ + var objectName = $(e.currentTarget).attr('data-recipe-pos-name'); + var objectId = $(e.currentTarget).attr('data-recipe-pos-id'); + + bootbox.confirm({ + message: L('Are you sure to delete recipe ingredient "#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/recipes_pos/' + objectId, + function(result) + { + window.location.href = U('/recipe/' + Grocy.EditObjectId); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/recipeposform.js b/public/viewjs/recipeposform.js new file mode 100644 index 00000000..7f38c51b --- /dev/null +++ b/public/viewjs/recipeposform.js @@ -0,0 +1,97 @@ +$('#save-recipe-pos-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = $('#recipe-pos-form').serializeJSON(); + jsonData.recipe_id = Grocy.EditObjectParentId; + console.log(jsonData); + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('add-object/recipes_pos', jsonData, + function(result) + { + window.location.href = U('/recipe/' + Grocy.EditObjectParentId); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + else + { + Grocy.Api.Post('edit-object/recipes_pos/' + Grocy.EditObjectId, jsonData, + function(result) + { + window.location.href = U('/recipe/' + Grocy.EditObjectParentId); + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +Grocy.Components.ProductPicker.GetPicker().on('change', function(e) +{ + var productId = $(e.target).val(); + + if (productId) + { + Grocy.Components.ProductCard.Refresh(productId); + + Grocy.Api.Get('stock/get-product-details/' + productId, + function (productDetails) + { + $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); + $('#amount').focus(); + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); + +if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) +{ + Grocy.Components.ProductPicker.GetInputElement().focus(); +} +Grocy.Components.ProductPicker.GetPicker().trigger('change'); + +$('#amount').on('focus', function(e) +{ + if (Grocy.Components.ProductPicker.GetValue().length === 0) + { + Grocy.Components.ProductPicker.GetInputElement().focus(); + } + else + { + $(this).select(); + } +}); + +$('#recipe-pos-form input').keyup(function (event) +{ + Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); +}); + +$('#recipe-pos-form input').keydown(function (event) +{ + if (event.keyCode === 13) //Enter + { + if (document.getElementById('recipe-pos-form').checkValidity() === false) //There is at least one validation error + { + event.preventDefault(); + return false; + } + else + { + $('#save-recipe-pos-button').click(); + } + } +}); diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js new file mode 100644 index 00000000..54689654 --- /dev/null +++ b/public/viewjs/recipes.js @@ -0,0 +1,58 @@ +var recipesTables = $('#recipes-table').DataTable({ + 'paginate': false, + 'order': [[1, 'asc']], + 'columnDefs': [ + { 'orderable': false, 'targets': 0 } + ], + 'language': JSON.parse(L('datatables_localization')), + 'scrollY': false, + 'colReorder': true, + 'stateSave': true +}); + +$("#search").on("keyup", function() +{ + var value = $(this).val(); + if (value === "all") + { + value = ""; + } + + recipesTables.search(value).draw(); +}); + +$(document).on('click', '.recipe-delete-button', function(e) +{ + var objectName = $(e.currentTarget).attr('data-recipe-name'); + var objectId = $(e.currentTarget).attr('data-recipe-id'); + + bootbox.confirm({ + message: L('Are you sure to delete recipe "#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/recipes/' + objectId, + function(result) + { + window.location.href = U('/recipes'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/routes.php b/routes.php index 58c7f6ce..aba8932a 100644 --- a/routes.php +++ b/routes.php @@ -34,6 +34,11 @@ $app->group('', function() $this->get('/shoppinglist', 'Grocy\Controllers\StockController:ShoppingList'); $this->get('/shoppinglistitem/{itemId}', 'Grocy\Controllers\StockController:ShoppingListItemEditForm'); + // Recipe routes + $this->get('/recipes', 'Grocy\Controllers\RecipesController:Overview'); + $this->get('/recipe/{recipeId}', 'Grocy\Controllers\RecipesController:RecipeEditForm'); + $this->get('/recipe/{recipeId}/pos/{recipePosId}', 'Grocy\Controllers\RecipesController:RecipePosEditForm'); + // Habit routes $this->get('/habitsoverview', 'Grocy\Controllers\HabitsController:Overview'); $this->get('/habittracking', 'Grocy\Controllers\HabitsController:TrackHabitExecution'); @@ -41,7 +46,7 @@ $app->group('', function() $this->get('/habits', 'Grocy\Controllers\HabitsController:HabitsList'); $this->get('/habit/{habitId}', 'Grocy\Controllers\HabitsController:HabitEditForm'); - // Batterry routes + // Battery routes $this->get('/batteriesoverview', 'Grocy\Controllers\BatteriesController:Overview'); $this->get('/batterytracking', 'Grocy\Controllers\BatteriesController:TrackChargeCycle'); diff --git a/services/RecipesService.php b/services/RecipesService.php new file mode 100644 index 00000000..02824635 --- /dev/null +++ b/services/RecipesService.php @@ -0,0 +1,18 @@ +DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); + } + + public function GetRecipesSumFulfillment() + { + $sql = 'SELECT * from recipes_fulfillment_sum'; + return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); + } +} diff --git a/views/components/productpicker.blade.php b/views/components/productpicker.blade.php index 1e008bb6..caf0f3ad 100644 --- a/views/components/productpicker.blade.php +++ b/views/components/productpicker.blade.php @@ -3,8 +3,9 @@ @endpush @php if(empty($disallowAddProductWorkflows)) { $disallowAddProductWorkflows = false; } @endphp +@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp -
+
+
{{ $L('A name is required') }}
+
+ +
+ + +
+ + + + +
+ +
+

+ {{ $L('Ingredients list') }} + + {{ $L('Add') }} + +

+ + + + + + + + + + + @if($mode == "edit") + @foreach($recipePositions as $recipePosition) + + + + + + + @endforeach + @endif + +
#{{ $L('Product') }}{{ $L('Amount') }}{{ $L('Note') }}
+ + + + + + + + {{ FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->name }} + + {{ $recipePosition->amount }} {{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->qu_id_stock)->name }} + @if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $recipePosition->id)->need_fullfiled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock') }} @endif + + @if(strlen($recipePosition->note) > 50) + {{ substr($recipePosition->note, 0, 50) }}... + @else + {{ $recipePosition->note }} + @endif +
+
+ +@stop diff --git a/views/recipeposform.blade.php b/views/recipeposform.blade.php new file mode 100644 index 00000000..2b6cbe8e --- /dev/null +++ b/views/recipeposform.blade.php @@ -0,0 +1,55 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $L('Edit recipe ingredient')) +@else + @section('title', $L('Add recipe ingredient')) +@endif + +@section('viewJsName', 'recipeposform') + +@section('content') +
+
+

@yield('title')

+

{{ $L('Recipe') }} {{ $recipe->name }}

+ + + + @if($mode == 'edit') + + @endif + +
+ + @php $prefillByName = ''; if($mode=='edit') { $prefillByName = FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name; } @endphp + @include('components.productpicker', array( + 'products' => $products, + 'nextInputSelector' => '#amount', + 'prefillByName' => $prefillByName + )) + +
+ + +
{{ $L('This cannot be negative') }}
+
+ +
+ + +
+ + + +
+
+ +
+ @include('components.productcard') +
+
+@stop diff --git a/views/recipes.blade.php b/views/recipes.blade.php new file mode 100644 index 00000000..6435f45f --- /dev/null +++ b/views/recipes.blade.php @@ -0,0 +1,59 @@ +@extends('layout.default') + +@section('title', $L('Recipes')) +@section('activeNav', 'recipes') +@section('viewJsName', 'recipes') + +@section('content') +
+
+

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

+
+
+ +
+
+ + +
+
+ +
+
+ + + + + + + + + + @foreach($recipes as $recipe) + + + + + + @endforeach + +
#{{ $L('Name') }}{{ $L('Requirements fulfilled') }}
+ + + + + + + + {{ $recipe->name }} + + @if(FindObjectInArrayByPropertyValue($recipesSumFulfillment, 'recipe_id', $recipe->id)->need_fullfiled == 1){{ $L('Yes') }}@else{{ $L('No') }}@endif +
+
+
+@stop diff --git a/yarn.lock b/yarn.lock index b7ba3097..ce8815ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -169,6 +169,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.17.3" resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.17.3.tgz#dfb96408ccc46775155f7369190c5d4b2016fe5c"