diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 01097e47..963d9f87 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -242,13 +242,29 @@ class StockApiController extends BaseApiController public function AddMissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $this->StockService->AddMissingProductsToShoppingList(); + $requestBody = $request->getParsedBody(); + + $listId = 1; + if (array_key_exists('list_id', $requestBody) && !empty($requestBody['list_id']) && is_numeric($requestBody['list_id'])) + { + $listId = intval($requestBody['list_id']); + } + + $this->StockService->AddMissingProductsToShoppingList($listId); return $this->EmptyApiResponse($response); } public function ClearShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $this->StockService->ClearShoppingList(); + $requestBody = $request->getParsedBody(); + + $listId = 1; + if (array_key_exists('list_id', $requestBody) && !empty($requestBody['list_id']) && is_numeric($requestBody['list_id'])) + { + $listId = intval($requestBody['list_id']); + } + + $this->StockService->ClearShoppingList($listId); return $this->EmptyApiResponse($response); } diff --git a/controllers/StockController.php b/controllers/StockController.php index 7551e2c2..bccb00cb 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -59,12 +59,20 @@ class StockController extends BaseController public function ShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { + $listId = 1; + if (isset($request->getQueryParams()['list'])) + { + $listId = $request->getQueryParams()['list']; + } + return $this->AppContainer->view->render($response, 'shoppinglist', [ - 'listItems' => $this->Database->shopping_list(), + 'listItems' => $this->Database->shopping_list()->where('shopping_list_id = :1', $listId), 'products' => $this->Database->products()->orderBy('name'), 'quantityunits' => $this->Database->quantity_units()->orderBy('name'), 'missingProducts' => $this->StockService->GetMissingProducts(), - 'productGroups' => $this->Database->product_groups()->orderBy('name') + 'productGroups' => $this->Database->product_groups()->orderBy('name'), + 'shoppingLists' => $this->Database->shopping_lists()->orderBy('name'), + 'selectedShoppingListId' => $listId ]); } @@ -187,14 +195,14 @@ class StockController extends BaseController { if ($args['itemId'] == 'new') { - return $this->AppContainer->view->render($response, 'shoppinglistform', [ + return $this->AppContainer->view->render($response, 'shoppinglistitemform', [ 'products' => $this->Database->products()->orderBy('name'), 'mode' => 'create' ]); } else { - return $this->AppContainer->view->render($response, 'shoppinglistform', [ + return $this->AppContainer->view->render($response, 'shoppinglistitemform', [ 'listItem' => $this->Database->shopping_list($args['itemId']), 'products' => $this->Database->products()->orderBy('name'), 'mode' => 'edit' @@ -202,6 +210,23 @@ class StockController extends BaseController } } + public function ShoppingListEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + if ($args['listId'] == 'new') + { + return $this->AppContainer->view->render($response, 'shoppinglistform', [ + 'mode' => 'create' + ]); + } + else + { + return $this->AppContainer->view->render($response, 'shoppinglistform', [ + 'shoppingList' => $this->Database->shopping_lists($args['listId']), + 'mode' => 'edit' + ]); + } + } + public function Journal(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { return $this->AppContainer->view->render($response, 'stockjournal', [ diff --git a/grocy.openapi.json b/grocy.openapi.json index f7830b35..25fc2eb2 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1330,10 +1330,29 @@ }, "/stock/shoppinglist/add-missing-products": { "post": { - "summary": "Adds currently missing products (below defined min. stock amount) to the shopping list", + "summary": "Adds currently missing products (below defined min. stock amount) to the given shopping list", "tags": [ "Stock" ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list_id": { + "type": "integer", + "description": "The shopping list to use, when omitted, the default shopping list (with id 1) is used" + } + }, + "example": { + "list_id": 2 + } + } + } + } + }, "responses": { "204": { "description": "The operation was successful" @@ -1343,10 +1362,29 @@ }, "/stock/shoppinglist/clear": { "post": { - "summary": "Removes all items from the shopping list", + "summary": "Removes all items from the given shopping list", "tags": [ "Stock" ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list_id": { + "type": "integer", + "description": "The shopping list id to clear, when omitted, the default shopping list (with id 1) is used" + } + }, + "example": { + "list_id": 2 + } + } + } + } + }, "responses": { "204": { "description": "The operation was successful" @@ -1964,6 +2002,7 @@ "locations", "quantity_units", "shopping_list", + "shopping_lists", "recipes", "recipes_pos", "recipes_nestings", diff --git a/localization/en/strings.php b/localization/en/strings.php index 800a25df..ce6dad29 100644 --- a/localization/en/strings.php +++ b/localization/en/strings.php @@ -151,7 +151,7 @@ return array( 'Edit recipe ingredient' => 'Edit recipe ingredient', 'Are you sure to delete recipe "#1"?' => 'Are you sure to delete recipe "#1"?', 'Are you sure to delete recipe ingredient "#1"?' => 'Are you sure to delete recipe ingredient "#1"?', - 'Are you sure to empty the shopping list?' => 'Are you sure to empty the shopping list?', + 'Are you sure to empty shopping list "#1"?' => 'Are you sure to empty shopping list "#1"?', 'Clear list' => 'Clear list', 'Requirements fulfilled' => 'Requirements fulfilled', 'Put missing products on shopping list' => 'Put missing products on shopping list', diff --git a/migrations/0062.sql b/migrations/0062.sql new file mode 100644 index 00000000..a939b70a --- /dev/null +++ b/migrations/0062.sql @@ -0,0 +1,14 @@ +ALTER TABLE shopping_list +ADD shopping_list_id INT DEFAULT 1; + +CREATE TABLE shopping_lists ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + name TEXT NOT NULL UNIQUE, + description TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) +); + +INSERT INTO shopping_lists + (name) +VALUES + ('Default'); diff --git a/migrations/0063.php b/migrations/0063.php new file mode 100644 index 00000000..e0796524 --- /dev/null +++ b/migrations/0063.php @@ -0,0 +1,13 @@ +DatabaseService->GetDbConnection(); + +$defaultShoppingList = $this->Database->shopping_lists()->where('id = 1')->fetch(); +$defaultShoppingList->update(array( + 'name' => $localizationService->Localize('Shopping list') +)); diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js index 3d737a39..9f33557f 100644 --- a/public/viewjs/shoppinglist.js +++ b/public/viewjs/shoppinglist.js @@ -51,6 +51,12 @@ $("#status-filter").on("change", function() shoppingListTable.column(4).search(value).draw(); }); +$("#selected-shopping-list").on("change", function() +{ + var value = $(this).val(); + window.location.href = U('/shoppinglist?list=' + value); +}); + $(".status-filter-button").on("click", function() { var value = $(this).data("status-filter"); @@ -58,7 +64,43 @@ $(".status-filter-button").on("click", function() $("#status-filter").trigger("change"); }); -$(document).on('click', '.shoppinglist-delete-button', function (e) +$("#delete-selected-shopping-list").on("click", function() +{ + var objectName = $("#selected-shopping-list option:selected").text(); + var objectId = $("#selected-shopping-list").val(); + + bootbox.confirm({ + message: L('Are you sure to delete shopping list "#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.Delete('objects/shopping_lists/' + objectId, {}, + function (result) + { + window.location.href = U('/shoppinglist'); + }, + function (xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); + +$(document).on('click', '.shoppinglist-delete-button', function(e) { e.preventDefault(); @@ -85,10 +127,10 @@ $(document).on('click', '.shoppinglist-delete-button', function (e) $(document).on('click', '#add-products-below-min-stock-amount', function(e) { - Grocy.Api.Post('stock/shoppinglist/add-missing-products', { }, + Grocy.Api.Post('stock/shoppinglist/add-missing-products', { "list_id": $("#selected-shopping-list").val() }, function(result) { - window.location.href = U('/shoppinglist'); + window.location.href = U('/shoppinglist?list=' + $("#selected-shopping-list").val()); }, function(xhr) { @@ -100,7 +142,7 @@ $(document).on('click', '#add-products-below-min-stock-amount', function(e) $(document).on('click', '#clear-shopping-list', function(e) { bootbox.confirm({ - message: L('Are you sure to empty the shopping list?'), + message: L('Are you sure to empty shopping list "#1"?', $("#selected-shopping-list option:selected").text()), buttons: { confirm: { label: L('Yes'), @@ -117,7 +159,7 @@ $(document).on('click', '#clear-shopping-list', function(e) { Grocy.FrontendHelpers.BeginUiBusy(); - Grocy.Api.Post('stock/shoppinglist/clear', { }, + Grocy.Api.Post('stock/shoppinglist/clear', { "list_id": $("#selected-shopping-list").val() }, function(result) { $('#shoppinglist-table tbody tr').fadeOut(500, function() diff --git a/public/viewjs/shoppinglistform.js b/public/viewjs/shoppinglistform.js index 9b18689f..7cfa3bfe 100644 --- a/public/viewjs/shoppinglistform.js +++ b/public/viewjs/shoppinglistform.js @@ -1,115 +1,61 @@ -$('#save-shoppinglist-button').on('click', function(e) +$('#save-shopping-list-button').on('click', function(e) { e.preventDefault(); - var jsonData = $('#shoppinglist-form').serializeJSON(); - Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form"); + var jsonData = $('#shopping-list-form').serializeJSON(); + Grocy.FrontendHelpers.BeginUiBusy("shopping-list-form"); if (Grocy.EditMode === 'create') { - Grocy.Api.Post('objects/shopping_list', jsonData, + Grocy.Api.Post('objects/shopping_lists', jsonData, function(result) { window.location.href = U('/shoppinglist'); }, function(xhr) { - Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); - console.error(xhr); + Grocy.FrontendHelpers.EndUiBusy("shopping-list-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) } ); } else { - Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData, + Grocy.Api.Put('objects/shopping_lists/' + Grocy.EditObjectId, jsonData, function(result) { window.location.href = U('/shoppinglist'); }, function(xhr) { - Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); - console.error(xhr); + Grocy.FrontendHelpers.EndUiBusy("shopping-list-form"); + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) } ); } }); -Grocy.Components.ProductPicker.GetPicker().on('change', function(e) +$('#shopping-list-form input').keyup(function(event) { - var productId = $(e.target).val(); - - if (productId) - { - Grocy.Components.ProductCard.Refresh(productId); - - Grocy.Api.Get('stock/products/' + productId, - function (productDetails) - { - $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); - - if (productDetails.product.allow_partial_units_in_stock == 1) - { - $("#amount").attr("min", "0.01"); - $("#amount").attr("step", "0.01"); - $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); - } - else - { - $("#amount").attr("min", "1"); - $("#amount").attr("step", "1"); - $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); - } - - $('#amount').focus(); - Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); - }, - function(xhr) - { - console.error(xhr); - } - ); - } + Grocy.FrontendHelpers.ValidateForm('shopping-list-form'); }); -Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); -Grocy.Components.ProductPicker.GetInputElement().focus(); - -if (Grocy.EditMode === "edit") -{ - 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(); - } -}); - -$('#shoppinglist-form input').keyup(function (event) -{ - Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); -}); - -$('#shoppinglist-form input').keydown(function (event) +$('#shopping-list-form input').keydown(function (event) { if (event.keyCode === 13) //Enter { event.preventDefault(); - if (document.getElementById('shoppinglist-form').checkValidity() === false) //There is at least one validation error + if (document.getElementById('shopping-list-form').checkValidity() === false) //There is at least one validation error { return false; } else { - $('#save-shoppinglist-button').click(); + $('#save-shopping-list-button').click(); } } }); + +$('#name').focus(); +Grocy.FrontendHelpers.ValidateForm('shopping-list-form'); diff --git a/public/viewjs/shoppinglistitemform.js b/public/viewjs/shoppinglistitemform.js new file mode 100644 index 00000000..81964bba --- /dev/null +++ b/public/viewjs/shoppinglistitemform.js @@ -0,0 +1,116 @@ +$('#save-shoppinglist-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonData = $('#shoppinglist-form').serializeJSON(); + jsonData.shopping_list_id = GetUriParam("list"); + Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form"); + + if (Grocy.EditMode === 'create') + { + Grocy.Api.Post('objects/shopping_list', jsonData, + function(result) + { + window.location.href = U('/shoppinglist?list=' + GetUriParam("list")); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); + console.error(xhr); + } + ); + } + else + { + Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData, + function(result) + { + window.location.href = U('/shoppinglist?list=' + GetUriParam("list")); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy("shoppinglist-form"); + 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/products/' + productId, + function (productDetails) + { + $('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); + + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', 0.01.toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1')); + } + + $('#amount').focus(); + Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); +Grocy.Components.ProductPicker.GetInputElement().focus(); + +if (Grocy.EditMode === "edit") +{ + 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(); + } +}); + +$('#shoppinglist-form input').keyup(function (event) +{ + Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); +}); + +$('#shoppinglist-form input').keydown(function (event) +{ + if (event.keyCode === 13) //Enter + { + event.preventDefault(); + + if (document.getElementById('shoppinglist-form').checkValidity() === false) //There is at least one validation error + { + return false; + } + else + { + $('#save-shoppinglist-button').click(); + } + } +}); diff --git a/routes.php b/routes.php index feed6b91..cb456c25 100644 --- a/routes.php +++ b/routes.php @@ -41,6 +41,7 @@ $app->group('', function() { $this->get('/shoppinglist', '\Grocy\Controllers\StockController:ShoppingList'); $this->get('/shoppinglistitem/{itemId}', '\Grocy\Controllers\StockController:ShoppingListItemEditForm'); + $this->get('/shoppinglist/{listId}', '\Grocy\Controllers\StockController:ShoppingListEditForm'); } // Recipe routes diff --git a/services/StockService.php b/services/StockService.php index cb3cd92a..17e010ed 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -430,7 +430,7 @@ class StockService extends BaseService return $this->Database->lastInsertId(); } - public function AddMissingProductsToShoppingList() + public function AddMissingProductsToShoppingList($listId = 1) { $missingProducts = $this->GetMissingProducts(); foreach ($missingProducts as $missingProduct) @@ -444,7 +444,8 @@ class StockService extends BaseService if ($alreadyExistingEntry->amount < $amountToAdd) { $alreadyExistingEntry->update(array( - 'amount' => $amountToAdd + 'amount' => $amountToAdd, + 'shopping_list_id' => $listId )); } } @@ -452,16 +453,17 @@ class StockService extends BaseService { $shoppinglistRow = $this->Database->shopping_list()->createRow(array( 'product_id' => $missingProduct->id, - 'amount' => $amountToAdd + 'amount' => $amountToAdd, + 'shopping_list_id' => $listId )); $shoppinglistRow->save(); } } } - public function ClearShoppingList() + public function ClearShoppingList($listId = 1) { - $this->Database->shopping_list()->delete(); + $this->Database->shopping_list()->where('shopping_list_id = :1', $listId)->delete(); } private function ProductExists($productId) diff --git a/views/shoppinglist.blade.php b/views/shoppinglist.blade.php index 4e3f15e1..e62af4e1 100644 --- a/views/shoppinglist.blade.php +++ b/views/shoppinglist.blade.php @@ -20,8 +20,8 @@

@yield('title') - - {{ $L('Add') }} + + {{ $L('Add item') }} {{ $L('Clear list') }} @@ -37,6 +37,26 @@

+
+
+ + +
+
+
+
+ {{ $L('New shopping list') }} + + + {{ $L('Delete shopping list') }} + +
+
+
@@ -67,7 +87,7 @@ @foreach($listItems as $listItem) - + diff --git a/views/shoppinglistform.blade.php b/views/shoppinglistform.blade.php index 0feb96de..b0328703 100644 --- a/views/shoppinglistform.blade.php +++ b/views/shoppinglistform.blade.php @@ -1,56 +1,40 @@ @extends('layout.default') @if($mode == 'edit') - @section('title', $L('Edit shopping list item')) + @section('title', $L('Edit shopping list')) @else - @section('title', $L('Create shopping list item')) + @section('title', $L('Create shopping list')) @endif @section('viewJsName', 'shoppinglistform') @section('content')
-
+

@yield('title')

@if($mode == 'edit') - + @endif -
- - @php if($mode == 'edit') { $productId = $listItem->product_id; } else { $productId = ''; } @endphp - @include('components.productpicker', array( - 'products' => $products, - 'nextInputSelector' => '#amount', - 'isRequired' => false, - 'prefillById' => $productId - )) - - @php if($mode == 'edit') { $value = $listItem->amount; } else { $value = 1; } @endphp - @include('components.numberpicker', array( - 'id' => 'amount', - 'label' => 'Amount', - 'hintId' => 'amount_qu_unit', - 'min' => 0, - 'value' => $value, - 'invalidFeedback' => $L('The amount cannot be lower than #1', '1') - )) +
- - + + +
{{ $L('A name is required') }}
- +
+ + +
+ +
- -
- @include('components.productcard') -
@stop diff --git a/views/shoppinglistitemform.blade.php b/views/shoppinglistitemform.blade.php new file mode 100644 index 00000000..e34cc28f --- /dev/null +++ b/views/shoppinglistitemform.blade.php @@ -0,0 +1,56 @@ +@extends('layout.default') + +@if($mode == 'edit') + @section('title', $L('Edit shopping list item')) +@else + @section('title', $L('Create shopping list item')) +@endif + +@section('viewJsName', 'shoppinglistitemform') + +@section('content') +
+
+

@yield('title')

+ + + + @if($mode == 'edit') + + @endif + +
+ + @php if($mode == 'edit') { $productId = $listItem->product_id; } else { $productId = ''; } @endphp + @include('components.productpicker', array( + 'products' => $products, + 'nextInputSelector' => '#amount', + 'isRequired' => false, + 'prefillById' => $productId + )) + + @php if($mode == 'edit') { $value = $listItem->amount; } else { $value = 1; } @endphp + @include('components.numberpicker', array( + 'id' => 'amount', + 'label' => 'Amount', + 'hintId' => 'amount_qu_unit', + 'min' => 0, + 'value' => $value, + 'invalidFeedback' => $L('The amount cannot be lower than #1', '1') + )) + +
+ + +
+ + + +
+
+ +
+ @include('components.productcard') +
+
+@stop