diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index 73b30c94..ee5d1df9 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -25,6 +25,7 @@ - When adding or editing a recipe ingredient, a dialog is now used instead of switching between pages (thanks @kriddles) ### Meal plan improvements/fixes +- It's now possible to add notes per day (in the dropdown of the add button in the header of each day column) - Added that the calories per serving are now also shown - Added that the total costs and calories per day are displayed in the header of each day column - Fixed that when `FEATURE_FLAG_STOCK_PRICE_TRACKING` was set to `false`, prices were still shown (thanks @kriddles) diff --git a/controllers/RecipesController.php b/controllers/RecipesController.php index 93b20cd3..466fa120 100644 --- a/controllers/RecipesController.php +++ b/controllers/RecipesController.php @@ -132,8 +132,20 @@ class RecipesController extends BaseController public function MealPlan(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $recipes = $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll(); - $events = array(); + + foreach($this->Database->meal_plan_notes() as $mealPlanNote) + { + $events[] = array( + 'id' => intval($mealPlanNote['id']) * -1, + 'title' => '', + 'start' => $mealPlanNote['day'], + 'date_format' => 'date', + 'note' => json_encode($mealPlanNote), + 'type' => 'note' + ); + } + foreach($this->Database->meal_plan() as $mealPlanEntry) { $recipe = FindObjectInArrayByPropertyValue($recipes, 'id', $mealPlanEntry['recipe_id']); @@ -149,7 +161,8 @@ class RecipesController extends BaseController 'start' => $mealPlanEntry['day'], 'date_format' => 'date', 'recipe' => json_encode($recipe), - 'mealPlanEntry' => json_encode($mealPlanEntry) + 'mealPlanEntry' => json_encode($mealPlanEntry), + 'type' => 'recipe' ); } diff --git a/grocy.openapi.json b/grocy.openapi.json index c49ce067..c07a282e 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -3272,7 +3272,8 @@ "userfields", "userentities", "userobjects", - "meal_plan" + "meal_plan", + "meal_plan_notes" ] }, "ExposedEntityButNoListing": { diff --git a/localization/strings.pot b/localization/strings.pot index 1d6f9881..9513456d 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1633,3 +1633,12 @@ msgstr "" msgid "Are you sure to delete API key \"%s\"?" msgstr "" + +msgid "Add note" +msgstr "" + +msgid "Add note to %s" +msgstr "" + +msgid "per day" +msgstr "" diff --git a/migrations/0096.sql b/migrations/0096.sql new file mode 100644 index 00000000..d3e0fc69 --- /dev/null +++ b/migrations/0096.sql @@ -0,0 +1,8 @@ +CREATE TABLE meal_plan_notes ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + day DATE NOT NULL, + note TEXT, + row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), + + UNIQUE(day) +); diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 489a930c..0569cfd2 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -29,7 +29,14 @@ var calendar = $("#calendar").fullCalendar({ UpdateUriParam("week", view.start.format("YYYY-MM-DD")); } - $(".fc-day-header").prepend(''); + $(".fc-day-header").prepend('\ +
\ + \ + \ + \ +
'); var weekRecipeName = view.start.year().toString() + "-" + ((view.start.week() - 1).toString().padStart(2, "0")).toString(); var weekRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", weekRecipeName); @@ -63,88 +70,105 @@ var calendar = $("#calendar").fullCalendar({ }, "eventRender": function(event, element) { - var recipe = JSON.parse(event.recipe); - if (recipe === null || recipe === undefined) - { - return false; - } - - var mealPlanEntry = JSON.parse(event.mealPlanEntry); - var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", recipe.id); - element.removeClass("fc-event"); element.addClass("text-center"); - element.attr("data-recipe", event.recipe); - element.attr("data-meal-plan-entry", event.mealPlanEntry); - - var recipeOrderMissingButtonDisabledClasses = ""; - if (resolvedRecipe.need_fulfilled_with_shopping_list == 1) + if (event.type == "recipe") { - recipeOrderMissingButtonDisabledClasses = "disabled"; - } - - var recipeConsumeButtonDisabledClasses = ""; - if (resolvedRecipe.need_fulfilled == 0) - { - recipeConsumeButtonDisabledClasses = "disabled"; - } - - var fulfillmentInfoHtml = __t('Enough in stock'); - var fulfillmentIconHtml = ''; - if (resolvedRecipe.need_fulfilled != 1) - { - fulfillmentInfoHtml = __t('Not enough in stock'); - var fulfillmentIconHtml = ''; - } - var costsAndCaloriesPerServing = "" - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) - { - costsAndCaloriesPerServing = '
' + resolvedRecipe.costs + ' / ' + resolvedRecipe.calories + ' kcal ' + __t('per serving') + '
'; - } - else - { - costsAndCaloriesPerServing = '
' + resolvedRecipe.calories + ' kcal ' + __t('per serving') + '
'; - } - - element.html(' \ -
\ -
' + recipe.name + '
\ -
' + __n(mealPlanEntry.servings, "%s serving", "%s servings") + '
\ -
' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '
\ - ' + costsAndCaloriesPerServing + ' \ -
\ - \ - \ - \ - \ -
\ -
'); - - if (recipe.picture_file_name && !recipe.picture_file_name.isEmpty()) - { - element.html(element.html() + '
') - } - - var dayRecipeName = event.start.format("YYYY-MM-DD"); - if (!$("#day-summary-" + dayRecipeName).length) // This runs for every event/recipe, so maybe multiple times per day, so only add the day summary once - { - var dayRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", dayRecipeName); - if (dayRecipe != null) + var recipe = JSON.parse(event.recipe); + if (recipe === null || recipe === undefined) { - var dayRecipeResolved = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", dayRecipe.id); - - var costsAndCaloriesPerDay = "" - if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) - { - costsAndCaloriesPerDay = '
' + dayRecipeResolved.costs + ' / ' + dayRecipeResolved.calories + ' kcal ' + '
'; - } - else - { - costsAndCaloriesPerDay = '
' + dayRecipeResolved.calories + ' kcal ' + '
'; - } - $(".fc-day-header[data-date='" + dayRecipeName + "']").append('
' + costsAndCaloriesPerDay + '
'); + return false; } + + var mealPlanEntry = JSON.parse(event.mealPlanEntry); + var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", recipe.id); + + element.attr("data-recipe", event.recipe); + element.attr("data-meal-plan-entry", event.mealPlanEntry); + + var recipeOrderMissingButtonDisabledClasses = ""; + if (resolvedRecipe.need_fulfilled_with_shopping_list == 1) + { + recipeOrderMissingButtonDisabledClasses = "disabled"; + } + + var recipeConsumeButtonDisabledClasses = ""; + if (resolvedRecipe.need_fulfilled == 0) + { + recipeConsumeButtonDisabledClasses = "disabled"; + } + + var fulfillmentInfoHtml = __t('Enough in stock'); + var fulfillmentIconHtml = ''; + if (resolvedRecipe.need_fulfilled != 1) + { + fulfillmentInfoHtml = __t('Not enough in stock'); + var fulfillmentIconHtml = ''; + } + var costsAndCaloriesPerServing = "" + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + { + costsAndCaloriesPerServing = '
' + resolvedRecipe.costs + ' / ' + resolvedRecipe.calories + ' kcal ' + __t('per serving') + '
'; + } + else + { + costsAndCaloriesPerServing = '
' + resolvedRecipe.calories + ' kcal ' + __t('per serving') + '
'; + } + + element.html('\ +
\ +
' + recipe.name + '
\ +
' + __n(mealPlanEntry.servings, "%s serving", "%s servings") + '
\ +
' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '
\ + ' + costsAndCaloriesPerServing + ' \ +
\ + \ + \ + \ + \ +
\ +
'); + + if (recipe.picture_file_name && !recipe.picture_file_name.isEmpty()) + { + element.html(element.html() + '
') + } + + var dayRecipeName = event.start.format("YYYY-MM-DD"); + if (!$("#day-summary-" + dayRecipeName).length) // This runs for every event/recipe, so maybe multiple times per day, so only add the day summary once + { + var dayRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", dayRecipeName); + if (dayRecipe != null) + { + var dayRecipeResolved = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", dayRecipe.id); + + var costsAndCaloriesPerDay = "" + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) + { + costsAndCaloriesPerDay = '
' + dayRecipeResolved.costs + ' / ' + dayRecipeResolved.calories + ' kcal ' + __t('per day') + '
'; + } + else + { + costsAndCaloriesPerDay = '
' + dayRecipeResolved.calories + ' kcal ' + __t('per day') + '
'; + } + $(".fc-day-header[data-date='" + dayRecipeName + "']").append('
' + costsAndCaloriesPerDay + '
'); + } + } + } + else if (event.type == "note") + { + var note = JSON.parse(event.note); + + element.attr("data-note", event.note); + + element.html('\ +
\ +
' + note.note + '
\ +
\ + \ +
\ +
'); } }, "eventAfterAllRender": function(view) @@ -162,7 +186,7 @@ var calendar = $("#calendar").fullCalendar({ $(document).on("click", ".add-recipe-button", function(e) { - var day = $(this).parent().data("date"); + var day = $(this).parent().parent().data("date"); $("#add-recipe-modal-title").text(__t("Add recipe to %s", day.toString())); $("#day").val(day.toString()); @@ -171,11 +195,27 @@ $(document).on("click", ".add-recipe-button", function(e) Grocy.FrontendHelpers.ValidateForm("add-recipe-form"); }); +$(document).on("click", ".add-note-button", function(e) +{ + var day = $(this).parent().parent().parent().data("date"); + + $("#add-note-modal-title").text(__t("Add note to %s", day.toString())); + $("#day").val(day.toString()); + $("#note").val(""); + $("#add-note-modal").modal("show"); + Grocy.FrontendHelpers.ValidateForm("add-note-form"); +}); + $("#add-recipe-modal").on("shown.bs.modal", function(e) { Grocy.Components.RecipePicker.GetInputElement().focus(); }) +$("#add-note-modal").on("shown.bs.modal", function (e) +{ + $("#note").focus(); +}) + $(document).on("click", ".remove-recipe-button", function(e) { var mealPlanEntry = JSON.parse($(this).parents(".fc-h-event:first").attr("data-meal-plan-entry")); @@ -192,6 +232,22 @@ $(document).on("click", ".remove-recipe-button", function(e) ); }); +$(document).on("click", ".remove-note-button", function(e) +{ + var note = JSON.parse($(this).parents(".fc-h-event:first").attr("data-note")); + + Grocy.Api.Delete('objects/meal_plan_notes/' + note.id.toString(), { }, + function(result) + { + window.location.reload(); + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); +}); + $('#save-add-recipe-button').on('click', function(e) { e.preventDefault(); @@ -213,6 +269,29 @@ $('#save-add-recipe-button').on('click', function(e) ); }); +$('#save-add-note-button').on('click', function(e) +{ + e.preventDefault(); + + if (document.getElementById("add-note-form").checkValidity() === false) //There is at least one validation error + { + return false; + } + + var jsonData = $('#add-note-form').serializeJSON(); + jsonData.day = $("#day").val(); + Grocy.Api.Post('objects/meal_plan_notes', jsonData, + function(result) + { + window.location.reload(); + }, + function(xhr) + { + Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) + } + ); +}); + Grocy.Components.RecipePicker.GetInputElement().keydown(function(event) { if (event.keyCode === 13) //Enter diff --git a/views/mealplan.blade.php b/views/mealplan.blade.php index 03a03d99..22b1e9ce 100644 --- a/views/mealplan.blade.php +++ b/views/mealplan.blade.php @@ -36,7 +36,7 @@