From 4d21668265f2eb8fa59e04a66b8d91b0f58e6ab3 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 14 Nov 2021 16:19:52 +0100 Subject: [PATCH] Added the possibility to merge chores --- changelog/65_3.1.3_2021-11-14.md | 2 + controllers/ChoresApiController.php | 20 +++++++++ grocy.openapi.json | 43 +++++++++++++++++++ localization/strings.pot | 17 +++++++- public/viewjs/chores.js | 25 +++++++++++ public/viewjs/products.js | 2 +- routes.php | 1 + services/ChoresService.php | 34 +++++++++++++++ views/chores.blade.php | 66 +++++++++++++++++++++++++++++ 9 files changed, 208 insertions(+), 2 deletions(-) diff --git a/changelog/65_3.1.3_2021-11-14.md b/changelog/65_3.1.3_2021-11-14.md index d22bca3e..53113a78 100644 --- a/changelog/65_3.1.3_2021-11-14.md +++ b/changelog/65_3.1.3_2021-11-14.md @@ -3,6 +3,7 @@ - Added the products average price as a (hidden by default) column on the stock overview page - Added a new "Presets for new products" stock setting for the "Default due days" option of new products - When adding (purchase) a product with "Default due days after freezing" set directly to a freezer location, the due date is now prefilled by that (instead of the normal "Default due days") (thanks @grahamc for the initial work on this) +- Chores can now be merged (new dropdown menu item on the chores list page) - Fixed that the labels of context-/more-menu items were not readable in Night Mode (thanks @corbolais) - Fixed that "Label per unit" stock entry labels (on purchase) weren't unique per unit - Fixed that the "Add as new product" productpicker workflow, started from the shopping list item form, always selected the default shopping list after finishing the flow @@ -13,5 +14,6 @@ - Fixed that the "Stay logged in permanently" checkbox on the login page had no effect (thanks @0) ### API +- New endpoint `/chores/{choreIdToKeep}/merge/{choreIdToRemove}` for merging chores - Endpoint `/stock/products/{productId}/add` API endpoint`: The (optional) request body parameter `print_stock_label` was renamed to `stock_label_type` - Fixed that backslashes were not allowed in API query filters diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index 6994602f..17252c2c 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -131,4 +131,24 @@ class ChoresApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function MergeChores(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); + + try + { + if (filter_var($args['choreIdToKeep'], FILTER_VALIDATE_INT) === false || filter_var($args['choreIdToRemove'], FILTER_VALIDATE_INT) === false) + { + throw new \Exception('Provided {choreIdToKeep} or {choreIdToRemove} is not a valid integer'); + } + + $this->ApiResponse($response, $this->getChoresService()->MergeChores($args['choreIdToKeep'], $args['choreIdToRemove'])); + return $this->EmptyApiResponse($response); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index b170a7ea..0c86be19 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -3779,6 +3779,49 @@ } } }, + "/chores/{choreIdToKeep}/merge/{choreIdToRemove}": { + "post": { + "summary": "Merges two chores into one", + "tags": [ + "Chores" + ], + "parameters": [ + { + "in": "path", + "name": "choreIdToKeep", + "required": true, + "description": "A valid chore id of the chore to keep", + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "choreIdToRemove", + "required": true, + "description": "A valid chore id of the chore to remove", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Invalid chore id)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error400" + } + } + } + } + } + } + }, "/batteries": { "get": { "summary": "Returns all batteries incl. the next estimated charge time per battery", diff --git a/localization/strings.pot b/localization/strings.pot index 497a006e..287f327d 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2048,7 +2048,7 @@ msgstr "" msgid "Product to remove" msgstr "" -msgid "Error while merging products" +msgid "Error while merging" msgstr "" msgid "After merging, this product will be kept" @@ -2244,3 +2244,18 @@ msgstr "" msgid "This is the default which will be prefilled on purchase" msgstr "" + +msgid "Merge chores" +msgstr "" + +msgid "Chore to keep" +msgstr "" + +msgid "After merging, this chore will be kept" +msgstr "" + +msgid "Chore to remove" +msgstr "" + +msgid "After merging, all occurences of this chore will be replaced by the kept chore (means this chore will not exist anymore)" +msgstr "" diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js index d45ebd4e..37a4958d 100644 --- a/public/viewjs/chores.js +++ b/public/viewjs/chores.js @@ -79,3 +79,28 @@ if (GetUriParam('include_disabled')) { $("#show-disabled").prop('checked', true); } + +$(".merge-chores-button").on("click", function(e) +{ + var choreId = $(e.currentTarget).attr("data-chore-id"); + $("#merge-chores-keep").val(choreId); + $("#merge-chores-remove").val(""); + $("#merge-chores-modal").modal("show"); +}); + +$("#merge-chores-save-button").on("click", function() +{ + var choreIdToKeep = $("#merge-chores-keep").val(); + var choreIdToRemove = $("#merge-chores-remove").val(); + + Grocy.Api.Post("chores/" + choreIdToKeep.toString() + "/merge/" + choreIdToRemove.toString(), {}, + function(result) + { + window.location.href = U('/chores'); + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while merging', xhr.response); + } + ); +}); diff --git a/public/viewjs/products.js b/public/viewjs/products.js index 18b2b3af..35807f72 100644 --- a/public/viewjs/products.js +++ b/public/viewjs/products.js @@ -147,7 +147,7 @@ $("#merge-products-save-button").on("click", function() }, function(xhr) { - Grocy.FrontendHelpers.ShowGenericError('Error while merging products', xhr.response); + Grocy.FrontendHelpers.ShowGenericError('Error while merging', xhr.response); } ); }); diff --git a/routes.php b/routes.php index 85bf50cf..e3b201a5 100644 --- a/routes.php +++ b/routes.php @@ -238,6 +238,7 @@ $app->group('/api', function (RouteCollectorProxy $group) { $group->post('/chores/executions/{executionId}/undo', '\Grocy\Controllers\ChoresApiController:UndoChoreExecution'); $group->post('/chores/executions/calculate-next-assignments', '\Grocy\Controllers\ChoresApiController:CalculateNextExecutionAssignments'); $group->get('/chores/{choreId}/printlabel', '\Grocy\Controllers\ChoresApiController:ChorePrintLabel'); + $group->post('/chores/{choreIdToKeep}/merge/{choreIdToRemove}', '\Grocy\Controllers\ChoresApiController:MergeChores'); //Printing $group->get('/print/shoppinglist/thermal', '\Grocy\Controllers\PrintApiController:PrintShoppingListThermal'); diff --git a/services/ChoresService.php b/services/ChoresService.php index 7f265bfb..6f3651c6 100644 --- a/services/ChoresService.php +++ b/services/ChoresService.php @@ -209,6 +209,40 @@ class ChoresService extends BaseService ]); } + public function MergeChores(int $choreIdToKeep, int $choreIdToRemove) + { + if (!$this->ChoreExists($choreIdToKeep)) + { + throw new \Exception('$choreIdToKeep does not exist or is inactive'); + } + + if (!$this->ChoreExists($choreIdToRemove)) + { + throw new \Exception('$choreIdToRemove does not exist or is inactive'); + } + + if ($choreIdToKeep == $choreIdToRemove) + { + throw new \Exception('$choreIdToKeep cannot equal $choreIdToRemove'); + } + + $this->getDatabaseService()->GetDbConnectionRaw()->beginTransaction(); + try + { + $choreToKeep = $this->getDatabase()->chores($choreIdToKeep); + $choreToRemove = $this->getDatabase()->chores($choreIdToRemove); + + $this->getDatabaseService()->ExecuteDbStatement('UPDATE chores_log SET chore_id = ' . $choreIdToKeep . ' WHERE chore_id = ' . $choreIdToRemove); + $this->getDatabaseService()->ExecuteDbStatement('DELETE FROM chores WHERE id = ' . $choreIdToRemove); + } + catch (Exception $ex) + { + $this->getDatabaseService()->GetDbConnectionRaw()->rollback(); + throw $ex; + } + $this->getDatabaseService()->GetDbConnectionRaw()->commit(); + } + private function ChoreExists($choreId) { $choreRow = $this->getDatabase()->chores()->where('id = :1', $choreId)->fetch(); diff --git a/views/chores.blade.php b/views/chores.blade.php index 25ee3ccb..6b23055c 100644 --- a/views/chores.blade.php +++ b/views/chores.blade.php @@ -116,6 +116,21 @@ title="{{ $__t('Delete this item') }}"> + {{ $chore->name }} @@ -138,4 +153,55 @@ + + @stop