Added the possibility to merge chores

This commit is contained in:
Bernd Bestel 2021-11-14 16:19:52 +01:00
parent 693dcc1020
commit 4d21668265
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
9 changed files with 208 additions and 2 deletions

View File

@ -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

View File

@ -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());
}
}
}

View File

@ -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",

View File

@ -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 ""

View File

@ -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);
}
);
});

View File

@ -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);
}
);
});

View File

@ -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');

View File

@ -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();

View File

@ -116,6 +116,21 @@
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i>
</a>
<div class="dropdown d-inline-block">
<button class="btn btn-sm btn-light text-secondary"
type="button"
data-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="table-inline-menu dropdown-menu dropdown-menu-right">
<a class="dropdown-item merge-chores-button"
data-chore-id="{{ $chore->id }}"
type="button"
href="#">
<span class="dropdown-item-text">{{ $__t('Merge') }}</span>
</a>
</div>
</div>
</td>
<td>
{{ $chore->name }}
@ -138,4 +153,55 @@
</table>
</div>
</div>
<div class="modal fade"
id="merge-chores-modal"
tabindex="-1">
<div class="modal-dialog">
<div class="modal-content text-center">
<div class="modal-header">
<h4 class="modal-title w-100">{{ $__t('Merge chores') }}</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label for="merge-chores-keep">{{ $__t('Chore to keep') }}&nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('After merging, this chore will be kept') }}"></i>
</label>
<select class="custom-control custom-select"
id="merge-chores-keep">
<option></option>
@foreach($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="merge-chores-remove">{{ $__t('Chore to remove') }}&nbsp;<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('After merging, all occurences of this chore will be replaced by the kept chore (means this chore will not exist anymore)') }}"></i>
</label>
<select class="custom-control custom-select"
id="merge-chores-remove">
<option></option>
@foreach($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option>
@endforeach
</select>
</div>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="merge-chores-save-button"
type="button"
class="btn btn-primary"
data-dismiss="modal">{{ $__t('OK') }}</button>
</div>
</div>
</div>
</div>
@stop