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 -