diff --git a/changelog/47_UNRELEASED_xxxx-xx-xx.md b/changelog/47_UNRELEASED_xxxx-xx-xx.md index 391d86bf..8f960733 100644 --- a/changelog/47_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/47_UNRELEASED_xxxx-xx-xx.md @@ -5,6 +5,7 @@ - => Can be configured under Master data / Userfields - New feature: Meal planning - Simple approach for the beginning (more to come): A week view where you can add recipes for each day (new menu entry in the sidebar, below calendar) + - Of course it's also possible to put missing things directly on the shopping list from there, also for a complete week at once - General improvements - The "expires soon" or "due soon" days (yelllow bar at the top of each overview page) can now be configured - => New settings page for each area under the settings icon at the top right diff --git a/controllers/RecipesController.php b/controllers/RecipesController.php index abaf2166..fc6d2921 100644 --- a/controllers/RecipesController.php +++ b/controllers/RecipesController.php @@ -19,7 +19,14 @@ class RecipesController extends BaseController public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $recipes = $this->Database->recipes()->orderBy('name'); + if (isset($request->getQueryParams()['include-internal'])) + { + $recipes = $this->Database->recipes()->orderBy('name'); + } + else + { + $recipes = $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name'); + } $recipesResolved = $this->RecipesService->GetRecipesResolved(); $selectedRecipe = null; @@ -71,7 +78,7 @@ class RecipesController extends BaseController $recipeId = $args['recipeId']; if ($recipeId == 'new') { - $newRecipe = $this->Database->recipes()->createRow(array( + $newRecipe = $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->createRow(array( 'name' => $this->LocalizationService->__t('New recipe') )); $newRecipe->save(); @@ -87,7 +94,7 @@ class RecipesController extends BaseController 'quantityunits' => $this->Database->quantity_units(), 'recipePositionsResolved' => $this->RecipesService->GetRecipesPosResolved(), 'recipesResolved' => $this->RecipesService->GetRecipesResolved(), - 'recipes' => $this->Database->recipes()->orderBy('name'), + 'recipes' => $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name'), 'recipeNestings' => $this->Database->recipes_nestings()->where('recipe_id', $recipeId), 'userfields' => $this->UserfieldsService->GetFields('recipes') ]); @@ -118,7 +125,7 @@ class RecipesController extends BaseController public function MealPlan(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $recipes = $this->Database->recipes()->fetchAll(); + $recipes = $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll(); $events = array(); foreach($this->Database->meal_plan() as $mealPlanEntry) @@ -135,7 +142,9 @@ class RecipesController extends BaseController return $this->AppContainer->view->render($response, 'mealplan', [ 'fullcalendarEventSources' => $events, - 'recipes' => $recipes + 'recipes' => $recipes, + 'internalRecipes' => $this->Database->recipes()->whereNot('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll(), + 'recipesResolved' => $this->RecipesService->GetRecipesResolved() ]); } } diff --git a/localization/strings.pot b/localization/strings.pot index 5456584e..8af2f098 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1236,3 +1236,11 @@ msgstr "" msgid "Add recipe to %s" msgstr "" + +msgid "%s serving" +msgid_plural "%s servings" +msgstr[0] "" +msgstr[1] "" + +msgid "Week costs" +msgstr "" diff --git a/migrations/0071.sql b/migrations/0071.sql new file mode 100644 index 00000000..2ac2d880 --- /dev/null +++ b/migrations/0071.sql @@ -0,0 +1,54 @@ +ALTER TABLE meal_plan +ADD servings INTEGER DEFAULT 1; + +ALTER TABLE recipes +ADD type TEXT DEFAULT 'normal'; + +CREATE INDEX ix_recipes ON recipes ( + name, + type +); + +CREATE TRIGGER create_internal_recipe AFTER INSERT ON meal_plan +BEGIN + -- 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; diff --git a/public/js/extensions.js b/public/js/extensions.js index 52f10875..f6bbac37 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -16,6 +16,11 @@ String.prototype.isEmpty = function() return (this.length === 0 || !this.trim()); }; +String.prototype.replaceAll = function(search, replacement) +{ + return this.replace(new RegExp(search, "g"), replacement); +}; + GetUriParam = function(key) { var currentUri = decodeURIComponent(window.location.search.substring(1)); @@ -72,3 +77,16 @@ $.extend($.expr[":"], return (elem.textContent || elem.innerText || "").toLowerCase().indexOf((match[3] || "").toLowerCase()) >= 0; } }); + +FindObjectInArrayByPropertyValue = function(array, propertyName, propertyValue) +{ + for (var i = 0; i < array.length; i++) + { + if (array[i][propertyName] == propertyValue) + { + return array[i]; + } + } + + return null; +} diff --git a/public/js/grocy.js b/public/js/grocy.js index 6deac66e..296cb403 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -482,15 +482,19 @@ $("#about-dialog-link").on("click", function() }); }); -$(".locale-number-format[data-format='currency']").each(function () +function RefreshLocaleNumberDisplay() { - $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency })); -}); + $(".locale-number-format[data-format='currency']").each(function() + { + $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency })); + }); -$(".locale-number-format[data-format='quantity-amount']").each(function () -{ - $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 3 })); -}); + $(".locale-number-format[data-format='quantity-amount']").each(function() + { + $(this).text(parseFloat($(this).text()).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 3 })); + }); +} +RefreshLocaleNumberDisplay(); $(document).on("click", ".easy-link-copy-textbox", function() { diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index aebfccd0..2e7c61cd 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -12,21 +12,72 @@ "viewRender": function(view) { $(".fc-day-header").append(''); + + var weekRecipeName = view.start.year().toString() + "-" + (view.start.week() - 1).toString(); + var weekRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", weekRecipeName); + + var weekCosts = 0; + var weekRecipeOrderMissingButtonHtml = ""; + if (weekRecipe !== null) + { + weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs; + + var weekRecipeOrderMissingButtonDisabledClasses = ""; + if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled_with_shopping_list == 1) + { + weekRecipeOrderMissingButtonDisabledClasses = "disabled"; + } + weekRecipeOrderMissingButtonHtml = '' + } + $(".fc-header-toolbar .fc-center").html("