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