diff --git a/app.php b/app.php index df81207c..448058a8 100644 --- a/app.php +++ b/app.php @@ -40,6 +40,15 @@ require_once __DIR__ . '/vendor/autoload.php'; require_once GROCY_DATAPATH . '/config.php'; require_once __DIR__ . '/config-dist.php'; //For not in own config defined values we use the default ones +// Definitions for disabled authentication mode +if (GROCY_DISABLE_AUTH === true) +{ + if (!defined('GROCY_USER_ID')) + { + define('GROCY_USER_ID', 1); + } +} + // Setup base application $appContainer = new \Slim\Container([ 'settings' => [ diff --git a/changelog/50_2.4.3_2019-07-06.md b/changelog/50_2.4.3_2019-07-06.md new file mode 100644 index 00000000..0a0674f1 --- /dev/null +++ b/changelog/50_2.4.3_2019-07-06.md @@ -0,0 +1,16 @@ +- Fixed the messed up message/toast after consuming a product from the stock overview page +- Fixed that "Track date only" chores were always tracked today, regardless of the given date +- Fixed that the "week costs" were wrong after removing a meal plan entry +- Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there) +- Fixed consuming recipes did not consume ingredients of the nested recipes +- Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) +- Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown) +- Improved date display for dates of today and no time (instead of the hours since midnight now just "Today" will be shown) +- Improved shopping list handling + - Items can now be switched between lists (there is a shopping list dropdown on the item edit page) + - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) +- Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) +- Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week) +- Added the possibility to undo a task (new button per task, only visible when task is already completed) and also a corresponding API endpoint +- Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen, defaults to `false` +- Added a new `config.php` setting `CALENDAR_FIRST_DAY_OF_WEEK` to be able to change the first day of a week used for calendar views (meal plan for example) in the frontend, defaults to locale default diff --git a/changelog/51_2.4.4_2019-07-07.md b/changelog/51_2.4.4_2019-07-07.md new file mode 100644 index 00000000..2b91b837 --- /dev/null +++ b/changelog/51_2.4.4_2019-07-07.md @@ -0,0 +1,6 @@ +- Fixed that price data (last price & chart) was not taken from inventory correction bookings, only purchases +- Fixed weekly chores were scheduled on the same day after execution +- Fixed that undone chores were also included in "Last tracked" +- Fixed the date-time-picker width was too narrow sometimes +- Improved that execution dates of "Track date only" chores will never display the time part +- Improved date display for products that never expire (again, there was a display problem after consuming an item on the stock overview page) diff --git a/config-dist.php b/config-dist.php index 72afae5a..c775054b 100644 --- a/config-dist.php +++ b/config-dist.php @@ -21,6 +21,11 @@ Setting('MODE', 'production'); # one of the other available localization files in the "/localization" directory Setting('CULTURE', 'en'); +# This is used to define the first day of a week for calendar views in the frontend, +# leave empty to use the locale default +# Needs to be a number where Sunday = 0, Monday = 1 and so forth +Setting('CALENDAR_FIRST_DAY_OF_WEEK', ''); + # To keep it simple: grocy does not handle any currency conversions, # this here is used to format all money values, # so doesn't matter really matter, but should be the @@ -45,6 +50,10 @@ Setting('DISABLE_URL_REWRITING', false); # You can set this to any overview you want. Example: Use recipes to set the homepage to the recipes overview. Setting('ENTRY_PAGE', 'stock'); +# Set this to true if you want to disable authentication / the login screen, +# places where user context is needed will then use the default (first existing) user +Setting('DISABLE_AUTH', false); + # Default user settings # These settings can be changed per user, here the defaults # are defined which are used when the user has not changed the setting so far diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index 9805a107..7b4c28bc 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -21,7 +21,7 @@ class ChoresApiController extends BaseApiController try { $trackedTime = date('Y-m-d H:i:s'); - if (array_key_exists('tracked_time', $requestBody) && IsIsoDateTime($requestBody['tracked_time'])) + if (array_key_exists('tracked_time', $requestBody) && (IsIsoDateTime($requestBody['tracked_time']) || IsIsoDate($requestBody['tracked_time']))) { $trackedTime = $requestBody['tracked_time']; } diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index 5cfa1ce2..3f3ee0cf 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -1,43 +1,68 @@ -RecipesService = new RecipesService(); - } - - protected $RecipesService; - - public function AddNotFulfilledProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) - { - $requestBody = $request->getParsedBody(); - $excludedProductIds = null; - - if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) - { - $excludedProductIds = $requestBody['excludedProductIds']; - } - - $this->RecipesService->AddNotFulfilledProductsToShoppingList($args['recipeId'], $excludedProductIds); - return $this->EmptyApiResponse($response); - } - - public function ConsumeRecipe(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) - { - try - { - $this->RecipesService->ConsumeRecipe($args['recipeId']); - return $this->EmptyApiResponse($response); - } - catch (\Exception $ex) - { - return $this->GenericErrorResponse($response, $ex->getMessage()); - } - } -} +RecipesService = new RecipesService(); + } + + protected $RecipesService; + + public function AddNotFulfilledProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $requestBody = $request->getParsedBody(); + $excludedProductIds = null; + + if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) + { + $excludedProductIds = $requestBody['excludedProductIds']; + } + + $this->RecipesService->AddNotFulfilledProductsToShoppingList($args['recipeId'], $excludedProductIds); + return $this->EmptyApiResponse($response); + } + + public function ConsumeRecipe(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $this->RecipesService->ConsumeRecipe($args['recipeId']); + return $this->EmptyApiResponse($response); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + + public function GetRecipeFulfillment(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + if(!isset($args['recipeId'])) + { + return $this->ApiResponse($this->RecipesService->GetRecipesResolved()); + } + + $recipeResolved = FindObjectInArrayByPropertyValue($this->RecipesService->GetRecipesResolved(), 'recipe_id', $args['recipeId']); + if(!$recipeResolved) + { + throw new \Exception('Recipe does not exist'); + } + else + { + return $this->ApiResponse($recipeResolved); + } + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } +} diff --git a/controllers/StockController.php b/controllers/StockController.php index a322b0ea..a395d07a 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -222,6 +222,7 @@ class StockController extends BaseController { return $this->AppContainer->view->render($response, 'shoppinglistitemform', [ 'products' => $this->Database->products()->orderBy('name'), + 'shoppingLists' => $this->Database->shopping_lists()->orderBy('name'), 'mode' => 'create' ]); } @@ -230,6 +231,7 @@ class StockController extends BaseController return $this->AppContainer->view->render($response, 'shoppinglistitemform', [ 'listItem' => $this->Database->shopping_list($args['itemId']), 'products' => $this->Database->products()->orderBy('name'), + 'shoppingLists' => $this->Database->shopping_lists()->orderBy('name'), 'mode' => 'edit' ]); } diff --git a/controllers/TasksApiController.php b/controllers/TasksApiController.php index fa9c8606..af4edda1 100644 --- a/controllers/TasksApiController.php +++ b/controllers/TasksApiController.php @@ -39,4 +39,17 @@ class TasksApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function UndoTask(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $this->TasksService->UndoTask($args['taskId']); + return $this->EmptyApiResponse($response); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index 196062a0..15e009fd 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1663,6 +1663,47 @@ } } }, + "/recipes/{recipeId}/fulfillment": { + "get": { + "summary": "Get stock fulfillment information for the given recipe", + "tags": [ + "Recipes" + ], + "parameters": [ + { + "in": "path", + "name": "recipeId", + "required": true, + "description": "A valid recipe id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A RecipeFulfillmentResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecipeFulfillmentResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/recipes/{recipeId}/consume": { "post": { "summary": "Consumes all products of the given recipe", @@ -1687,6 +1728,39 @@ } } }, + "/recipes/fulfillment": { + "get": { + "summary": "Get stock fulfillment information for all recipe", + "tags": [ + "Recipes" + ], + "responses": { + "200": { + "description": "An array of RecipeFulfillmentResponse objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecipeFulfillmentResponse" + } + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/chores": { "get": { "summary": "Returns all chores incl. the next estimated execution time per chore", @@ -2077,6 +2151,40 @@ } } }, + "/tasks/{taskId}/undo": { + "post": { + "summary": "Marks the given task as not completed", + "tags": [ + "Tasks" + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "description": "A valid task id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing task)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/calendar/ical": { "get": { "summary": "Returns the calendar in iCal format", @@ -2375,6 +2483,34 @@ "location_id": "4" } }, + "RecipeFulfillmentResponse": { + "type": "object", + "properties": { + "recipe_id": { + "type": "integer" + }, + "need_fulfilled": { + "type": "boolean" + }, + "need_fulfilled_with_shopping_list": { + "type": "boolean" + }, + "missing_products_count": { + "type": "integer" + }, + "costs": { + "type": "number", + "format": "double" + } + }, + "example": { + "recipe_id": "1", + "need_fulfilled": "0", + "need_fulfilled_with_shopping_list": "0", + "missing_products_count": "2", + "costs": "17.74" + } + }, "ProductDetailsResponse": { "type": "object", "properties": { diff --git a/localization/de/strings.po b/localization/de/strings.po index 099f89c6..9747f8c5 100644 --- a/localization/de/strings.po +++ b/localization/de/strings.po @@ -670,7 +670,7 @@ msgstr "" msgid "Removed all ingredients of recipe \"%s\" from stock" msgstr "" -"Alle Zutaten, die vom Rezept \"%s\" benötigt werden, wurdem aus dem Bestand " +"Alle Zutaten, die vom Rezept \"%s\" benötigt werden, wurden aus dem Bestand " "entfernt" msgid "Consume all ingredients needed by this recipe" @@ -1374,3 +1374,23 @@ msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" "Die Buchung hat nachfolgende abhängige Buchungen, rückgängig machen nicht " "möglich" + +msgid "per serving" +msgstr "pro Portion" + +msgid "Never" +msgstr "Nie" + +msgid "Today" +msgstr "Heute" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "Verbrauche %1$s %2$s als verdorben" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" +"Nicht alle Zutaten, die vom Rezept \"%s\" benötigt werden, sind vorrätig, es" +" wurde nichts aus dem Bestand entfernt" + +msgid "Undo task \"%s\"" +msgstr "Aufgabe \"%s\" rückgängig machen" diff --git a/localization/en_GB/strings.po b/localization/en_GB/strings.po index b2296527..9dec2974 100644 --- a/localization/en_GB/strings.po +++ b/localization/en_GB/strings.po @@ -1308,3 +1308,21 @@ msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/es/demo_data.po b/localization/es/demo_data.po index 1a2c784b..c86e7c2b 100644 --- a/localization/es/demo_data.po +++ b/localization/es/demo_data.po @@ -1,6 +1,7 @@ # Translators: # Bernd Bestel , 2019 # Fernando Sánchez , 2019 +# Ankue , 2019 # msgid "" msgstr "" @@ -8,7 +9,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Fernando Sánchez , 2019\n" +"Last-Translator: Ankue , 2019\n" "Language-Team: Spanish (https://www.transifex.com/grocy/teams/93189/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -37,8 +38,8 @@ msgstr "Nevera" msgid "Piece" msgid_plural "Pieces" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Pieza" +msgstr[1] "Piezas" msgid "Pack" msgid_plural "Packs" @@ -47,8 +48,8 @@ msgstr[1] "" msgid "Glass" msgid_plural "Glasses" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Vaso" +msgstr[1] "Vasos" msgid "Tin" msgid_plural "Tins" @@ -57,8 +58,8 @@ msgstr[1] "" msgid "Can" msgid_plural "Cans" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Lata" +msgstr[1] "Latas" msgid "Bunch" msgid_plural "Bunches" @@ -172,8 +173,8 @@ msgstr "Usuario de demostración" msgid "Gram" msgid_plural "Grams" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Gramo" +msgstr[1] "Gramos" msgid "Flour" msgstr "Harina" @@ -281,4 +282,4 @@ msgid "Swedish" msgstr "Sueco" msgid "Polish" -msgstr "" +msgstr "Polaco" diff --git a/localization/fr/strings.po b/localization/fr/strings.po index 56ddbc7d..3baffb66 100644 --- a/localization/fr/strings.po +++ b/localization/fr/strings.po @@ -5,6 +5,7 @@ # Cedric Octave , 2019 # Hydreliox Hydreliox , 2019 # Matthieu K, 2019 +# Mathieu Fortin , 2019 # msgid "" msgstr "" @@ -12,7 +13,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Matthieu K, 2019\n" +"Last-Translator: Mathieu Fortin , 2019\n" "Language-Team: French (https://www.transifex.com/grocy/teams/93189/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1369,4 +1370,22 @@ msgid "Marked task %s as completed on %s" msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" +msgstr "La réservation a des dépendances, impossible de revenir en arrière" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" msgstr "" diff --git a/localization/it/chore_types.po b/localization/it/chore_types.po index ddae56c3..25159fd9 100644 --- a/localization/it/chore_types.po +++ b/localization/it/chore_types.po @@ -1,5 +1,6 @@ # Translators: # Bernd Bestel , 2019 +# Matteo Piotto , 2019 # msgid "" msgstr "" @@ -7,7 +8,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Bernd Bestel , 2019\n" +"Last-Translator: Matteo Piotto , 2019\n" "Language-Team: Italian (https://www.transifex.com/grocy/teams/93189/it/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -23,10 +24,10 @@ msgid "dynamic-regular" msgstr "Regolatore dinamico" msgid "daily" -msgstr "" +msgstr "Giornalmente" msgid "weekly" -msgstr "" +msgstr "Settimanalmente" msgid "monthly" -msgstr "" +msgstr "Mensilmente" diff --git a/localization/nl/component_translations.po b/localization/nl/component_translations.po index 880c06b8..b0a5667f 100644 --- a/localization/nl/component_translations.po +++ b/localization/nl/component_translations.po @@ -1,5 +1,6 @@ # Translators: # Bernd Bestel , 2019 +# Giel Janssens , 2019 # msgid "" msgstr "" @@ -7,7 +8,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Bernd Bestel , 2019\n" +"Last-Translator: Giel Janssens , 2019\n" "Language-Team: Dutch (https://www.transifex.com/grocy/teams/93189/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -26,7 +27,7 @@ msgid "moment_locale" msgstr "nl" msgid "datatables_localization" -msgstr "" +msgstr "nl-NL" msgid "summernote_locale" msgstr "nl-NL" diff --git a/localization/no/strings.po b/localization/no/strings.po index 514286cd..632188b5 100644 --- a/localization/no/strings.po +++ b/localization/no/strings.po @@ -1357,3 +1357,21 @@ msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/pl/strings.po b/localization/pl/strings.po index 6afbd148..65bc20ec 100644 --- a/localization/pl/strings.po +++ b/localization/pl/strings.po @@ -1387,3 +1387,27 @@ msgstr "Dni \"blisko terminu wykonania zadania\"" msgid "Products" msgstr "Produkty" + +msgid "Marked task %s as completed on %s" +msgstr "Oznaczono zadanie %s jako wykonane dnia %s " + +msgid "Booking has subsequent dependent bookings, undo not possible" +msgstr "Rezerwacja ma kolejne zależne rezerwacje, nie można jej cofnąć" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/ru/strings.po b/localization/ru/strings.po index ed314ec0..cb1faea6 100644 --- a/localization/ru/strings.po +++ b/localization/ru/strings.po @@ -1391,3 +1391,27 @@ msgstr "Критерий для \"подходит срок задач\" в дн msgid "Products" msgstr "Продукты" + +msgid "Marked task %s as completed on %s" +msgstr "Пометить задачу \"%s\" как выполненную %s" + +msgid "Booking has subsequent dependent bookings, undo not possible" +msgstr "У данного действия есть зависимые действия, отмена невозможна" + +msgid "per serving" +msgstr "на порцию" + +msgid "Never" +msgstr "Никогда" + +msgid "Today" +msgstr "Сегодня" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "Употребить %1$s %2$s как испорченное" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "Не все ингредиенты рецепта \"%s\" есть в запасе, ничего не изъято" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/strings.pot b/localization/strings.pot index 56e6796b..a71486c5 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1268,3 +1268,21 @@ msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/middleware/ApiKeyAuthMiddleware.php b/middleware/ApiKeyAuthMiddleware.php index dd0bc62b..82487c03 100644 --- a/middleware/ApiKeyAuthMiddleware.php +++ b/middleware/ApiKeyAuthMiddleware.php @@ -22,7 +22,7 @@ class ApiKeyAuthMiddleware extends BaseMiddleware $route = $request->getAttribute('route'); $routeName = $route->getName(); - if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL) + if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) { define('GROCY_AUTHENTICATED', true); $response = $next($request, $response); diff --git a/middleware/SessionAuthMiddleware.php b/middleware/SessionAuthMiddleware.php index df7c7a11..5cf436cc 100644 --- a/middleware/SessionAuthMiddleware.php +++ b/middleware/SessionAuthMiddleware.php @@ -25,7 +25,7 @@ class SessionAuthMiddleware extends BaseMiddleware { $response = $next($request, $response); } - elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL) + elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) { $user = $sessionService->GetDefaultUser(); define('GROCY_AUTHENTICATED', true); diff --git a/migrations/0073.sql b/migrations/0073.sql new file mode 100644 index 00000000..01b41cf3 --- /dev/null +++ b/migrations/0073.sql @@ -0,0 +1,92 @@ +DROP TRIGGER create_internal_recipe; +CREATE TRIGGER create_internal_recipe AFTER INSERT ON meal_plan +BEGIN + /* This contains practically the same logic as the trigger remove_internal_recipe */ + + -- Create a recipe per day + DELETE FROM recipes + WHERE name = NEW.day + AND type = 'mealplan-day'; + + INSERT OR REPLACE INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), NEW.day, 'mealplan-day'); + + -- Create a recipe per week + DELETE FROM recipes + WHERE name = LTRIM(STRFTIME('%Y-%W', NEW.day), '0') + AND type = 'mealplan-week'; + + INSERT INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), LTRIM(STRFTIME('%Y-%W', NEW.day), '0'), 'mealplan-week'); + + -- Delete all current nestings entries for the day and week recipe + DELETE FROM recipes_nestings + WHERE recipe_id IN (SELECT id FROM recipes WHERE name = NEW.day AND type = 'mealplan-day') + OR recipe_id IN (SELECT id FROM recipes WHERE name = NEW.day AND type = 'mealplan-week'); + + -- Add all recipes for this day as included recipes in the day-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = NEW.day AND type = 'mealplan-day'), recipe_id, SUM(servings) + FROM meal_plan + WHERE day = NEW.day + GROUP BY recipe_id; + + -- Add all recipes for this week as included recipes in the week-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = LTRIM(STRFTIME('%Y-%W', NEW.day), '0') AND type = 'mealplan-week'), recipe_id, SUM(servings) + FROM meal_plan + WHERE STRFTIME('%Y-%W', day) = STRFTIME('%Y-%W', NEW.day) + GROUP BY recipe_id; +END; + +CREATE TRIGGER remove_internal_recipe AFTER DELETE ON meal_plan +BEGIN + /* This contains practically the same logic as the trigger create_internal_recipe */ + + -- Create a recipe per day + DELETE FROM recipes + WHERE name = OLD.day + AND type = 'mealplan-day'; + + INSERT OR REPLACE INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), OLD.day, 'mealplan-day'); + + -- Create a recipe per week + DELETE FROM recipes + WHERE name = LTRIM(STRFTIME('%Y-%W', OLD.day), '0') + AND type = 'mealplan-week'; + + INSERT INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), LTRIM(STRFTIME('%Y-%W', OLD.day), '0'), 'mealplan-week'); + + -- Delete all current nestings entries for the day and week recipe + DELETE FROM recipes_nestings + WHERE recipe_id IN (SELECT id FROM recipes WHERE name = OLD.day AND type = 'mealplan-day') + OR recipe_id IN (SELECT id FROM recipes WHERE name = OLD.day AND type = 'mealplan-week'); + + -- Add all recipes for this day as included recipes in the day-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = OLD.day AND type = 'mealplan-day'), recipe_id, SUM(servings) + FROM meal_plan + WHERE day = OLD.day + GROUP BY recipe_id; + + -- Add all recipes for this week as included recipes in the week-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = LTRIM(STRFTIME('%Y-%W', OLD.day), '0') AND type = 'mealplan-week'), recipe_id, SUM(servings) + FROM meal_plan + WHERE STRFTIME('%Y-%W', day) = STRFTIME('%Y-%W', OLD.day) + GROUP BY recipe_id; +END; diff --git a/migrations/0074.sql b/migrations/0074.sql new file mode 100644 index 00000000..faeb0b6a --- /dev/null +++ b/migrations/0074.sql @@ -0,0 +1,85 @@ +DROP VIEW recipes_pos_resolved; +CREATE VIEW recipes_pos_resolved +AS + +-- Multiplication by 1.0 to force conversion to float (REAL) + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled, + CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + CASE WHEN IFNULL(sc.amount, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 0 + +UNION + +-- Just add all recipe positions which should not be checked against stock with fulfilled need + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + 1 AS need_fulfilled, + 0 AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + 1 AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 1; diff --git a/migrations/0075.sql b/migrations/0075.sql new file mode 100644 index 00000000..8bcf7e8b --- /dev/null +++ b/migrations/0075.sql @@ -0,0 +1,2 @@ +ALTER TABLE shopping_list +ADD done INT DEFAULT 0; diff --git a/migrations/0076.sql b/migrations/0076.sql new file mode 100644 index 00000000..c2b6b9e0 --- /dev/null +++ b/migrations/0076.sql @@ -0,0 +1,87 @@ +DROP VIEW recipes_pos_resolved; +CREATE VIEW recipes_pos_resolved +AS + +-- Multiplication by 1.0 to force conversion to float (REAL) + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled, + CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + CASE WHEN IFNULL(sc.amount, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount, + rp.only_check_single_unit_in_stock +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 0 + +UNION + +-- Just add all recipe positions which should not be checked against stock with fulfilled need + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + 1 AS need_fulfilled, + 0 AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + 1 AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount, + rp.only_check_single_unit_in_stock +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 1; diff --git a/migrations/0077.sql b/migrations/0077.sql new file mode 100644 index 00000000..808db214 --- /dev/null +++ b/migrations/0077.sql @@ -0,0 +1,28 @@ +DROP VIEW chores_current; +CREATE VIEW chores_current +AS +SELECT + h.id AS chore_id, + MAX(l.tracked_time) AS last_tracked_time, + CASE h.period_type + WHEN 'manually' THEN '2999-12-31 23:59:59' + WHEN 'dynamic-regular' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day') + WHEN 'daily' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+1 day') + WHEN 'weekly' THEN + CASE + WHEN period_config LIKE '%sunday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 0') + WHEN period_config LIKE '%monday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 1') + WHEN period_config LIKE '%tuesday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 2') + WHEN period_config LIKE '%wednesday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 3') + WHEN period_config LIKE '%thursday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 4') + WHEN period_config LIKE '%friday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 5') + WHEN period_config LIKE '%saturday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 6') + END + WHEN 'monthly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+1 month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day') + END AS next_estimated_execution_time, + h.track_date_only +FROM chores h +LEFT JOIN chores_log l + ON h.id = l.chore_id + AND l.undone = 0 +GROUP BY h.id, h.period_days; diff --git a/public/css/grocy.css b/public/css/grocy.css index 1ec0fced..d9eb9949 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -270,7 +270,7 @@ html { } /* Third party component customizations - Tempus Dominus */ -.date-only-datetimepicker .bootstrap-datetimepicker-widget.dropdown-menu { +.bootstrap-datetimepicker-widget.dropdown-menu { width: auto !important; } diff --git a/public/js/extensions.js b/public/js/extensions.js index f6bbac37..269a2c88 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -37,6 +37,13 @@ GetUriParam = function(key) } }; +UpdateUriParam = function(key, value) +{ + var queryParameters = new URLSearchParams(location.search); + queryParameters.set(key, value); + window.history.replaceState({ }, "", decodeURIComponent(`${location.pathname}?${queryParameters}`)); +}; + IsTouchInputDevice = function() { if (("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch) diff --git a/public/js/grocy.js b/public/js/grocy.js index c87747b0..a44c9c6c 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -1,101 +1,4 @@ -Grocy.Translator = new Translator(Grocy.GettextPo); -__t = function(text, ...placeholderValues) -{ - if (Grocy.Mode === "dev") - { - var text2 = text; - Grocy.Api.Post('system/log-missing-localization', { "text": text2 }); - } - - return Grocy.Translator.__(text, ...placeholderValues) -} -__n = function(number, singularForm, pluralForm) -{ - if (Grocy.Mode === "dev") - { - var singularForm2 = singularForm; - Grocy.Api.Post('system/log-missing-localization', { "text": singularForm2 }); - } - - return Grocy.Translator.n__(singularForm, pluralForm, number, number) -} - -U = function(relativePath) -{ - return Grocy.BaseUrl.replace(/\/$/, '') + relativePath; -} - -if (!Grocy.ActiveNav.isEmpty()) -{ - var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); - menuItem.addClass('active-page'); - - var parentMenuSelector = menuItem.data("sub-menu-of"); - if (typeof parentMenuSelector !== "undefined") - { - $(parentMenuSelector).collapse("show"); - $(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page"); - } -} - -var observer = new MutationObserver(function(mutations) -{ - mutations.forEach(function(mutation) - { - if (mutation.attributeName === "class") - { - var attributeValue = $(mutation.target).prop(mutation.attributeName); - if (attributeValue.contains("sidenav-toggled")) - { - window.localStorage.setItem("sidebar_state", "collapsed"); - } - else - { - window.localStorage.setItem("sidebar_state", "expanded"); - } - } - }); -}); -observer.observe(document.body, { - attributes: true -}); -if (window.localStorage.getItem("sidebar_state") === "collapsed") -{ - $("#sidenavToggler").click(); -} - -$.timeago.settings.allowFuture = true; -RefreshContextualTimeago = function() -{ - $("time.timeago").each(function() - { - var element = $(this); - var timestamp = element.attr("datetime"); - element.timeago("update", timestamp); - }); -} -RefreshContextualTimeago(); - -toastr.options = { - toastClass: 'alert', - closeButton: true, - timeOut: 20000, - extendedTimeOut: 5000 -}; - -window.FontAwesomeConfig = { - searchPseudoElements: true -} - -// Don't show tooltips on touch input devices -if (IsTouchInputDevice()) -{ - var css = document.createElement("style"); - css.innerHTML = ".tooltip { display: none; }"; - document.body.appendChild(css); -} - -Grocy.Api = { }; +Grocy.Api = { }; Grocy.Api.Get = function(apiFunction, success, error) { var xhr = new XMLHttpRequest(); @@ -323,6 +226,124 @@ Grocy.Api.DeleteFile = function(fileName, group, success, error) xhr.send(); }; +Grocy.Translator = new Translator(Grocy.GettextPo); +__t = function(text, ...placeholderValues) +{ + if (Grocy.Mode === "dev") + { + var text2 = text; + Grocy.Api.Post('system/log-missing-localization', { "text": text2 }); + } + + return Grocy.Translator.__(text, ...placeholderValues) +} +__n = function(number, singularForm, pluralForm) +{ + if (Grocy.Mode === "dev") + { + var singularForm2 = singularForm; + Grocy.Api.Post('system/log-missing-localization', { "text": singularForm2 }); + } + + return Grocy.Translator.n__(singularForm, pluralForm, number, number) +} + +U = function(relativePath) +{ + return Grocy.BaseUrl.replace(/\/$/, '') + relativePath; +} + +if (!Grocy.ActiveNav.isEmpty()) +{ + var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); + menuItem.addClass('active-page'); + + var parentMenuSelector = menuItem.data("sub-menu-of"); + if (typeof parentMenuSelector !== "undefined") + { + $(parentMenuSelector).collapse("show"); + $(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page"); + } +} + +var observer = new MutationObserver(function(mutations) +{ + mutations.forEach(function(mutation) + { + if (mutation.attributeName === "class") + { + var attributeValue = $(mutation.target).prop(mutation.attributeName); + if (attributeValue.contains("sidenav-toggled")) + { + window.localStorage.setItem("sidebar_state", "collapsed"); + } + else + { + window.localStorage.setItem("sidebar_state", "expanded"); + } + } + }); +}); +observer.observe(document.body, { + attributes: true +}); +if (window.localStorage.getItem("sidebar_state") === "collapsed") +{ + $("#sidenavToggler").click(); +} + +$.timeago.settings.allowFuture = true; +RefreshContextualTimeago = function() +{ + $("time.timeago").each(function() + { + var element = $(this); + var timestamp = element.attr("datetime"); + + var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; + var isToday = timestamp && timestamp.length == 10 && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD"); + var isDateWithoutTime = element.hasClass("timeago-date-only"); + + if (isNever) + { + element.prev().text(__t("Never")); + } + else if (isToday) + { + element.text(__t("Today")); + } + else + { + element.timeago("update", timestamp); + } + + if (isDateWithoutTime) + { + element.prev().text(element.prev().text().substring(0, 10)); + } + }); +} +RefreshContextualTimeago(); + +toastr.options = { + toastClass: 'alert', + closeButton: true, + timeOut: 20000, + extendedTimeOut: 5000 +}; + +window.FontAwesomeConfig = { + searchPseudoElements: true +} + +// Don't show tooltips on touch input devices +if (IsTouchInputDevice()) +{ + var css = document.createElement("style"); + css.innerHTML = ".tooltip { display: none; }"; + document.body.appendChild(css); +} + Grocy.FrontendHelpers = { }; Grocy.FrontendHelpers.ValidateForm = function(formId) { diff --git a/public/viewjs/calendar.js b/public/viewjs/calendar.js index c6e27c3b..0745bdf5 100644 --- a/public/viewjs/calendar.js +++ b/public/viewjs/calendar.js @@ -1,4 +1,10 @@ -$("#calendar").fullCalendar({ +var firstDay = null; +if (!Grocy.CalendarFirstDayOfWeek.isEmpty()) +{ + firstDay = parseInt(Grocy.CalendarFirstDayOfWeek); +} + +$("#calendar").fullCalendar({ "themeSystem": "bootstrap4", "header": { "left": "month,basicWeek,listWeek", @@ -6,6 +12,7 @@ "right": "prev,next" }, "weekNumbers": true, + "firstDay": firstDay, "eventLimit": true, "eventSources": fullcalendarEventSources }); diff --git a/public/viewjs/components/batterycard.js b/public/viewjs/components/batterycard.js index 982a3733..1349be8d 100644 --- a/public/viewjs/components/batterycard.js +++ b/public/viewjs/components/batterycard.js @@ -8,13 +8,14 @@ Grocy.Components.BatteryCard.Refresh = function(batteryId) $('#batterycard-battery-name').text(batteryDetails.battery.name); $('#batterycard-battery-used_in').text(batteryDetails.battery.used_in); $('#batterycard-battery-last-charged').text((batteryDetails.last_charged || __t('never'))); - $('#batterycard-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || '')); + $('#batterycard-battery-last-charged-timeago').attr("datetime", batteryDetails.last_charged || ''); $('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0')); $('#batterycard-battery-edit-button').attr("href", U("/battery/" + batteryDetails.battery.id.toString())); $('#batterycard-battery-edit-button').removeClass("disabled"); EmptyElementWhenMatches('#batterycard-battery-last-charged-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }, function(xhr) { diff --git a/public/viewjs/components/chorecard.js b/public/viewjs/components/chorecard.js index d66b112e..90322ff8 100644 --- a/public/viewjs/components/chorecard.js +++ b/public/viewjs/components/chorecard.js @@ -7,7 +7,7 @@ Grocy.Components.ChoreCard.Refresh = function(choreId) { $('#chorecard-chore-name').text(choreDetails.chore.name); $('#chorecard-chore-last-tracked').text((choreDetails.last_tracked || __t('never'))); - $('#chorecard-chore-last-tracked-timeago').text($.timeago(choreDetails.last_tracked || '')); + $('#chorecard-chore-last-tracked-timeago').attr("datetime", choreDetails.last_tracked || ''); $('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0')); $('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || __t('Unknown'))); @@ -15,6 +15,7 @@ Grocy.Components.ChoreCard.Refresh = function(choreId) $('#chorecard-chore-edit-button').removeClass("disabled"); EmptyElementWhenMatches('#chorecard-chore-last-tracked-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }, function(xhr) { diff --git a/public/viewjs/components/datetimepicker.js b/public/viewjs/components/datetimepicker.js index c6fc63a6..82e5dc91 100644 --- a/public/viewjs/components/datetimepicker.js +++ b/public/viewjs/components/datetimepicker.js @@ -48,6 +48,15 @@ Grocy.Components.DateTimePicker.ChangeFormat = function(format) $(".datetimepicker").datetimepicker("destroy"); Grocy.Components.DateTimePicker.GetInputElement().data("format", format); Grocy.Components.DateTimePicker.Init(); + + if (format == "YYYY-MM-DD") + { + Grocy.Components.DateTimePicker.GetInputElement().addClass("date-only-datetimepicker"); + } + else + { + Grocy.Components.DateTimePicker.GetInputElement().removeClass("date-only-datetimepicker"); + } } var startDate = null; @@ -226,8 +235,9 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) Grocy.Components.DateTimePicker.GetInputElement().on('input', function(e) { - $('#datetimepicker-timeago').text($.timeago(Grocy.Components.DateTimePicker.GetValue())); + $('#datetimepicker-timeago').attr("datetime", Grocy.Components.DateTimePicker.GetValue()); EmptyElementWhenMatches('#datetimepicker-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }); $('.datetimepicker').on('update.datetimepicker', function(e) diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index c274493b..2562fea8 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -12,9 +12,9 @@ Grocy.Components.ProductCard.Refresh = function(productId) $('#productcard-product-stock-amount').text(stockAmount); $('#productcard-product-stock-qu-name').text(__n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); $('#productcard-product-last-purchased').text((productDetails.last_purchased || __t('never')).substring(0, 10)); - $('#productcard-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || '')); + $('#productcard-product-last-purchased-timeago').attr("datetime", productDetails.last_purchased || ''); $('#productcard-product-last-used').text((productDetails.last_used || __t('never')).substring(0, 10)); - $('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); + $('#productcard-product-last-used-timeago').attr("datetime", productDetails.last_used || ''); $('#productcard-product-location').text(productDetails.location.name); $('#productcard-product-spoil-rate').text(parseFloat(productDetails.spoil_rate_percent).toLocaleString(undefined, { style: "percent" })); @@ -71,6 +71,7 @@ Grocy.Components.ProductCard.Refresh = function(productId) EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', __t('timeago_nan')); EmptyElementWhenMatches('#productcard-product-last-used-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }, function(xhr) { diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index d3d1b062..33d30478 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -1,4 +1,12 @@ -var calendar = $("#calendar").fullCalendar({ +var firstRender = true; + +var firstDay = null; +if (!Grocy.CalendarFirstDayOfWeek.isEmpty()) +{ + firstDay = parseInt(Grocy.CalendarFirstDayOfWeek); +} + +var calendar = $("#calendar").fullCalendar({ "themeSystem": "bootstrap4", "header": { "left": "title", @@ -9,8 +17,18 @@ "eventLimit": true, "eventSources": fullcalendarEventSources, "defaultView": "basicWeek", + "firstDay": firstDay, "viewRender": function(view) { + if (firstRender) + { + firstRender = false + } + else + { + UpdateUriParam("week", view.start.format("YYYY-MM-DD")); + } + $(".fc-day-header").append(''); var weekRecipeName = view.start.year().toString() + "-" + (view.start.week() - 1).toString(); @@ -18,6 +36,7 @@ var weekCosts = 0; var weekRecipeOrderMissingButtonHtml = ""; + var weekRecipeConsumeButtonHtml = ""; if (weekRecipe !== null) { weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs; @@ -27,9 +46,15 @@ { weekRecipeOrderMissingButtonDisabledClasses = "disabled"; } + var weekRecipeConsumeButtonDisabledClasses = ""; + if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0) + { + weekRecipeConsumeButtonDisabledClasses = "disabled"; + } weekRecipeOrderMissingButtonHtml = '' + weekRecipeConsumeButtonHtml = '' } - $(".fc-header-toolbar .fc-center").html("

" + __t("Week costs") + ': ' + weekCosts.toString() + " " + weekRecipeOrderMissingButtonHtml + "

"); + $(".fc-header-toolbar .fc-center").html("

" + __t("Week costs") + ': ' + weekCosts.toString() + " " + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "

"); }, "eventRender": function(event, element) { @@ -49,6 +74,12 @@ recipeOrderMissingButtonDisabledClasses = "disabled"; } + var recipeConsumeButtonDisabledClasses = ""; + if (resolvedRecipe.need_fulfilled == 0) + { + recipeConsumeButtonDisabledClasses = "disabled"; + } + var fulfillmentInfoHtml = __t('Enough in stock'); var fulfillmentIconHtml = ''; if (resolvedRecipe.need_fulfilled != 1) @@ -62,10 +93,11 @@
' + recipe.name + '
\
' + __n(mealPlanEntry.servings, "%s serving", "%s servings") + '
\
' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '
\ -
' + resolvedRecipe.costs + '
\ +
' + resolvedRecipe.costs + ' ' + __t('per serving') + '
\
\ \ \ + \
\ '); @@ -77,6 +109,11 @@ "eventAfterAllRender": function(view) { RefreshLocaleNumberDisplay(); + + if (GetUriParam("week") !== undefined) + { + $("#calendar").fullCalendar("gotoDate", GetUriParam("week")); + } }, }); @@ -103,7 +140,7 @@ $(document).on("click", ".remove-recipe-button", function(e) Grocy.Api.Delete('objects/meal_plan/' + mealPlanEntry.id.toString(), { }, function(result) { - calendar.fullCalendar('removeEvents', [mealPlanEntry.id]); + window.location.reload(); }, function(xhr) { @@ -214,3 +251,44 @@ $(document).on('click', '.recipe-order-missing-button', function(e) } }); }); + +$(document).on('click', '.recipe-consume-button', function(e) +{ + var objectName = $(e.currentTarget).attr('data-recipe-name'); + var objectId = $(e.currentTarget).attr('data-recipe-id'); + + bootbox.confirm({ + message: __t('Are you sure to consume all ingredients needed by recipe "%s" (ingredients marked with "check only if a single unit is in stock" will be ignored)?', objectName), + buttons: { + confirm: { + label: __t('Yes'), + className: 'btn-success' + }, + cancel: { + label: __t('No'), + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.FrontendHelpers.BeginUiBusy(); + + Grocy.Api.Post('recipes/' + objectId + '/consume', { }, + function(result) + { + Grocy.FrontendHelpers.EndUiBusy(); + toastr.success(__t('Removed all ingredients of recipe "%s" from stock', objectName)); + }, + function(xhr) + { + toastr.warning(__t('Not all ingredients of recipe "%s" are in stock, nothing removed', objectName)); + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index c1dde598..624a87ff 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -187,6 +187,7 @@ $("#selectedRecipeConsumeButton").on('click', function(e) function(xhr) { Grocy.FrontendHelpers.EndUiBusy(); + toastr.warning(__t('Not all ingredients of recipe "%s" are in stock, nothing removed', objectName)); console.error(xhr); } ); diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js index 15ff902d..e625692b 100644 --- a/public/viewjs/shoppinglist.js +++ b/public/viewjs/shoppinglist.js @@ -259,6 +259,46 @@ $(document).on('click', '#shopping-list-stock-add-workflow-skip-button', functio window.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl); }); +$(document).on('click', '.order-listitem-button', function(e) +{ + e.preventDefault(); + + Grocy.FrontendHelpers.BeginUiBusy(); + + var listItemId = $(e.currentTarget).attr('data-item-id'); + + var done = 1; + if ($(e.currentTarget).attr('data-item-done') == 1) + { + done = 0; + } + + $(e.currentTarget).attr('data-item-done', done); + + Grocy.Api.Put('objects/shopping_list/' + listItemId, { 'done': done }, + function() + { + if (done == 1) + { + $('#shoppinglistitem-' + listItemId + '-row').addClass("text-muted"); + $('#shoppinglistitem-' + listItemId + '-row').addClass("text-strike-through"); + } + else + { + $('#shoppinglistitem-' + listItemId + '-row').removeClass("text-muted"); + $('#shoppinglistitem-' + listItemId + '-row').removeClass("text-strike-through"); + } + + Grocy.FrontendHelpers.EndUiBusy(); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); +}); + function OnListItemRemoved() { if ($(".shopping-list-stock-add-workflow-list-item-button").length === 0) diff --git a/public/viewjs/shoppinglistitemform.js b/public/viewjs/shoppinglistitemform.js index c7248a0d..c2113aad 100644 --- a/public/viewjs/shoppinglistitemform.js +++ b/public/viewjs/shoppinglistitemform.js @@ -3,7 +3,6 @@ e.preventDefault(); var jsonData = $('#shoppinglist-form').serializeJSON(); - jsonData.shopping_list_id = GetUriParam("list"); Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form"); if (Grocy.EditMode === 'create') @@ -11,7 +10,7 @@ Grocy.Api.Post('objects/shopping_list', jsonData, function(result) { - window.location.href = U('/shoppinglist?list=' + GetUriParam("list")); + window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); }, function(xhr) { @@ -25,7 +24,7 @@ Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData, function(result) { - window.location.href = U('/shoppinglist?list=' + GetUriParam("list")); + window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); }, function(xhr) { @@ -93,12 +92,12 @@ $('#amount').on('focus', function(e) } }); -$('#shoppinglist-form input').keyup(function (event) +$('#shoppinglist-form input').keyup(function(event) { Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); }); -$('#shoppinglist-form input').keydown(function (event) +$('#shoppinglist-form input').keydown(function(event) { if (event.keyCode === 13) //Enter { @@ -114,3 +113,8 @@ $('#shoppinglist-form input').keydown(function (event) } } }); + +if (GetUriParam("list") !== undefined) +{ + $("#shopping_list_id").val(GetUriParam("list")); +} diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index b8ac1e3d..316afa46 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -89,11 +89,10 @@ $(document).on('click', '.product-consume-button', function(e) Grocy.FrontendHelpers.BeginUiBusy(); var productId = $(e.currentTarget).attr('data-product-id'); - var productName = $(e.currentTarget).attr('data-product-name'); - var productQuName = $(e.currentTarget).attr('data-product-qu-name'); var consumeAmount = $(e.currentTarget).attr('data-consume-amount'); + var wasSpoiled = $(e.currentTarget).hasClass("product-consume-button-spoiled"); - Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount }, + Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled }, function() { Grocy.Api.Get('stock/products/' + productId, @@ -127,8 +126,9 @@ $(document).on('click', '.product-consume-button', function(e) } else { + $('#product-' + productId + '-qu-name').text(__n(newAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)); $('#product-' + productId + '-amount').parent().effect('highlight', { }, 500); - $('#product-' + productId + '-amount').fadeOut(500, function() + $('#product-' + productId + '-amount').fadeOut(500, function () { $(this).text(newAmount).fadeIn(500); }); @@ -156,10 +156,21 @@ $(document).on('click', '.product-consume-button', function(e) }); } + var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name); + if (wasSpoiled) + { + toastMessage += " (" + __t("Spoiled") + ")"; + } + Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(__t('Removed %1$s of %2$s from stock', consumeAmount, productQuName, productName)); - RefreshContextualTimeago(); + toastr.success(toastMessage); RefreshStatistics(); + + // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... + setTimeout(function () + { + RefreshContextualTimeago(); + }, 520); }, function(xhr) { @@ -214,14 +225,14 @@ $(document).on('click', '.product-open-button', function(e) } $('#product-' + productId + '-next-best-before-date').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-next-best-before-date').fadeOut(500, function () + $('#product-' + productId + '-next-best-before-date').fadeOut(500, function() { $(this).text(result.next_best_before_date).fadeIn(500); }); $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); $('#product-' + productId + '-opened-amount').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-opened-amount').fadeOut(500, function () + $('#product-' + productId + '-opened-amount').fadeOut(500, function() { $(this).text(__t('%s opened', result.stock_amount_opened)).fadeIn(500); }); @@ -233,8 +244,13 @@ $(document).on('click', '.product-open-button', function(e) Grocy.FrontendHelpers.EndUiBusy(); toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName)); - RefreshContextualTimeago(); RefreshStatistics(); + + // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... + setTimeout(function() + { + RefreshContextualTimeago(); + }, 600); }, function(xhr) { diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js index edbdd0c9..4b0f4220 100644 --- a/public/viewjs/tasks.js +++ b/public/viewjs/tasks.js @@ -102,6 +102,32 @@ $(document).on('click', '.do-task-button', function(e) ); }); +$(document).on('click', '.undo-task-button', function(e) +{ + e.preventDefault(); + + // Remove the focus from the current button + // to prevent that the tooltip stays until clicked anywhere else + document.activeElement.blur(); + + Grocy.FrontendHelpers.BeginUiBusy(); + + var taskId = $(e.currentTarget).attr('data-task-id'); + var taskName = $(e.currentTarget).attr('data-task-name'); + + Grocy.Api.Post('tasks/' + taskId + '/undo', { }, + function() + { + window.location.reload(); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); +}); + $(document).on('click', '.delete-task-button', function (e) { e.preventDefault(); diff --git a/routes.php b/routes.php index aeb1131a..f32726ef 100644 --- a/routes.php +++ b/routes.php @@ -173,7 +173,9 @@ $app->group('/api', function() if (GROCY_FEATURE_FLAG_RECIPES) { $this->post('/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist', '\Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList'); + $this->get('/recipes/{recipeId}/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); $this->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe'); + $this->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); } // Chores @@ -199,6 +201,7 @@ $app->group('/api', function() { $this->get('/tasks', '\Grocy\Controllers\TasksApiController:Current'); $this->post('/tasks/{taskId}/complete', '\Grocy\Controllers\TasksApiController:MarkTaskAsCompleted'); + $this->post('/tasks/{taskId}/undo', '\Grocy\Controllers\TasksApiController:UndoTask'); } // Calendar diff --git a/services/RecipesService.php b/services/RecipesService.php index 9011a6ce..966e1f5c 100644 --- a/services/RecipesService.php +++ b/services/RecipesService.php @@ -67,12 +67,12 @@ class RecipesService extends BaseService throw new \Exception('Recipe does not exist'); } - $recipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipeId)->fetchAll(); + $recipePositions = $this->Database->recipes_pos_resolved()->where('recipe_id', $recipeId)->fetchAll(); foreach ($recipePositions as $recipePosition) { if ($recipePosition->only_check_single_unit_in_stock == 0) { - $this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId); + $this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId); } } } diff --git a/services/StockService.php b/services/StockService.php index deed7afe..13101dec 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -82,7 +82,7 @@ class StockService extends BaseService $averageShelfLifeDays = intval($this->Database->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = null; - $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); + $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); if ($lastLogRow !== null && !empty($lastLogRow)) { $lastPrice = $lastLogRow->price; @@ -120,7 +120,7 @@ class StockService extends BaseService } $returnData = array(); - $rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); + $rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); foreach ($rows as $row) { $returnData[] = array( diff --git a/services/TasksService.php b/services/TasksService.php index a9262fdd..1219b4eb 100644 --- a/services/TasksService.php +++ b/services/TasksService.php @@ -26,6 +26,22 @@ class TasksService extends BaseService return true; } + public function UndoTask($taskId) + { + if (!$this->TaskExists($taskId)) + { + throw new \Exception('Task does not exist'); + } + + $taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch(); + $taskRow->update(array( + 'done' => 0, + 'done_timestamp' => null + )); + + return true; + } + private function TaskExists($taskId) { $taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch(); diff --git a/version.json b/version.json index bb67ee32..685ae57f 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "Version": "2.4.2", - "ReleaseDate": "2019-06-09" + "Version": "2.4.4", + "ReleaseDate": "2019-07-07" } diff --git a/views/choresjournal.blade.php b/views/choresjournal.blade.php index c97aae37..96cab621 100644 --- a/views/choresjournal.blade.php +++ b/views/choresjournal.blade.php @@ -55,8 +55,8 @@ @endif - {{ $choreLogEntry->tracked_time }} - + {{ $choreLogEntry->tracked_time }} + @if ($choreLogEntry->done_by_user_id !== null && !empty($choreLogEntry->done_by_user_id)) diff --git a/views/choresoverview.blade.php b/views/choresoverview.blade.php index 9d327ada..59a67a20 100644 --- a/views/choresoverview.blade.php +++ b/views/choresoverview.blade.php @@ -85,14 +85,14 @@ @if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY) {{ $curentChoreEntry->next_estimated_execution_time }} - + @else ... @endif {{ $curentChoreEntry->last_tracked_time }} - + @if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif diff --git a/views/inventory.blade.php b/views/inventory.blade.php index 42de3518..758118bc 100644 --- a/views/inventory.blade.php +++ b/views/inventory.blade.php @@ -59,7 +59,7 @@ @include('components.locationpicker', array( 'locations' => $locations, - 'hint' => 'This will apply to added products' + 'hint' => $__t('This will apply to added products') )) diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 95a6f2d1..d7d94a97 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -52,6 +52,7 @@ Grocy.ActiveNav = '@yield('activeNav', '')'; Grocy.Culture = '{{ GROCY_CULTURE }}'; Grocy.Currency = '{{ GROCY_CURRENCY }}'; + Grocy.CalendarFirstDayOfWeek = '{{ GROCY_CALENDAR_FIRST_DAY_OF_WEEK }}'; Grocy.GettextPo = {!! $GettextPo !!}; Grocy.UserSettings = {!! json_encode($userSettings) !!}; Grocy.FeatureFlags = {!! json_encode($featureFlags) !!}; diff --git a/views/recipes.blade.php b/views/recipes.blade.php index d16964c1..c4972dc9 100644 --- a/views/recipes.blade.php +++ b/views/recipes.blade.php @@ -121,10 +121,10 @@
{{ $selectedRecipe->name }}   - + - +    diff --git a/views/shoppinglist.blade.php b/views/shoppinglist.blade.php index c0c7b673..8a669370 100644 --- a/views/shoppinglist.blade.php +++ b/views/shoppinglist.blade.php @@ -85,8 +85,13 @@ @foreach($listItems as $listItem) - + + + + diff --git a/views/shoppinglistitemform.blade.php b/views/shoppinglistitemform.blade.php index be88b71b..06021d18 100644 --- a/views/shoppinglistitemform.blade.php +++ b/views/shoppinglistitemform.blade.php @@ -21,6 +21,15 @@
+
+ + +
+ @php if($mode == 'edit') { $productId = $listItem->product_id; } else { $productId = ''; } @endphp @include('components.productpicker', array( 'products' => $products, diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 8425b887..4b07bc0a 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -122,6 +122,14 @@ {{ $__t('Edit product') }} + + + {{ $__t('Consume %1$s of %2$s as spoiled', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }} +
@@ -129,7 +137,7 @@ {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} - {{ $currentStockEntry->amount }} {{ $__n($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }} + {{ $currentStockEntry->amount }} {{ $__n($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }} @if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif diff --git a/views/tasks.blade.php b/views/tasks.blade.php index 4c951a8a..b2fad076 100644 --- a/views/tasks.blade.php +++ b/views/tasks.blade.php @@ -73,11 +73,19 @@ @foreach($tasks as $task) - name) }}" + @if($task->done == 0) + name) }}" data-task-id="{{ $task->id }}" data-task-name="{{ $task->name }}"> + @else + name) }}" + data-task-id="{{ $task->id }}" + data-task-name="{{ $task->name }}"> + + + @endif