Implemented meal plan sections (closes #370)

This commit is contained in:
Bernd Bestel 2021-07-15 17:54:48 +02:00
parent 1bacd8e13d
commit 2d2700cacb
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
17 changed files with 798 additions and 248 deletions

View File

@ -2,7 +2,8 @@
> ⚠️ PHP 8 is now supported and from now on the only tested runtime version (although currently PHP 7.2 should still work).
### New feature: (Own) Product/stock entry/chores/batteries labels/barcodes ("grocycode")
### New feature: grocycode
#### (Own) Product/stock entry/chores/batteries labels/barcodes
- Print own labels/barcodes for products/stock entries/chores/batteries and then scan that code on every place a product/stock entry/chore/battery can be selected
- Can be printed (or downloaded) via
- The product/chore/battery edit page
@ -19,6 +20,11 @@
- https://github.com/grocy/grocy/blob/master/docs/label-printing.md
- (Thanks a lot @mistressofjellyfish for the initial work on this)
### New feature: Meal plan sections
- Split the meal plan into sections like Breakfast/Lunch/Dinner
- => New button "Configure sections" on the meal plan page to configure the sections (top right corner)
- => Each meal plan entry can be assigned to a section
### New feature: Shopping list thermal printer support
- The shopping list can now be printed on a thermal printer
- The printer must be compatible to the `ESC/POS` protocol and needs to be locally attached or network reachable to/by the machine hosting grocy (so the server)

View File

@ -8,16 +8,15 @@ class RecipesController extends BaseController
{
public function MealPlan(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
// Load +- 8 days to always include week boundaries
// Given date is always the first day of the week => load the coming week / 7 days
if (isset($request->getQueryParams()['week']) && IsIsoDate($request->getQueryParams()['week']))
{
$week = $request->getQueryParams()['week'];
$mealPlanWhereTimespan = "day BETWEEN DATE('$week', '-8 day') AND DATE('$week', '+8 day')";
$mealPlanWhereTimespan = "day BETWEEN DATE('$week') AND DATE('$week', '+7 days')";
}
else
{
$mealPlanWhereTimespan = "day BETWEEN DATE(DATE('now', 'localtime'), '-8 day') AND DATE(DATE('now', 'localtime'), '+8 day')";
$mealPlanWhereTimespan = "day BETWEEN DATE('now', 'localtime', 'weekday 0', '-7 days') AND DATE(DATE('now', 'localtime', 'weekday 0', '-7 days'), '+7 days')";
}
$recipes = $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll();
@ -58,7 +57,9 @@ class RecipesController extends BaseController
'recipesResolved' => $this->getRecipesService()->GetRecipesResolved2("recipe_id IN (SELECT recipe_id FROM meal_plan_internal_recipe_relation WHERE $mealPlanWhereTimespan)"),
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'mealplanSections' => $this->getDatabase()->meal_plan_sections()->orderBy('sort_number'),
'usedMealplanSections' => $this->getDatabase()->meal_plan_sections()->where("id IN (SELECT section_id FROM meal_plan WHERE $mealPlanWhereTimespan)")->orderBy('sort_number')
]);
}
@ -190,6 +191,30 @@ class RecipesController extends BaseController
return $this->renderPage($response, 'recipessettings');
}
public function MealPlanSectionEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
if ($args['sectionId'] == 'new')
{
return $this->renderPage($response, 'mealplansectionform', [
'mode' => 'create'
]);
}
else
{
return $this->renderPage($response, 'mealplansectionform', [
'mealplanSection' => $this->getDatabase()->meal_plan_sections($args['sectionId']),
'mode' => 'edit'
]);
}
}
public function MealPlanSectionsList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
return $this->renderPage($response, 'mealplansections', [
'mealplanSections' => $this->getDatabase()->meal_plan_sections()->where('id > 0')->orderBy('sort_number')
]);
}
public function __construct(\DI\Container $container)
{
parent::__construct($container);

View File

@ -5579,7 +5579,8 @@
"stock_log",
"stock",
"stock_current_locations",
"chores_log"
"chores_log",
"meal_plan_sections"
]
},
"ExposedEntityNoListing": {

View File

@ -377,3 +377,12 @@ msgstr ""
msgid "Finnish"
msgstr ""
msgid "Breakfast"
msgstr ""
msgid "Lunch"
msgstr ""
msgid "Dinner"
msgstr ""

View File

@ -2223,3 +2223,24 @@ msgstr ""
msgid "Stock entry"
msgstr ""
msgid "Configure sections"
msgstr ""
msgid "Meal plan sections"
msgstr ""
msgid "Create meal plan section"
msgstr ""
msgid "Sections will be ordered by that number on the meal plan"
msgstr ""
msgid "Edit meal plan section"
msgstr ""
msgid "Are you sure to delete meal plan section \"%s\"?"
msgstr ""
msgid "Section"
msgstr ""

View File

@ -69,7 +69,7 @@ BEGIN
GROUP BY product_id, product_qu_id;
-- Create a shadow recipe per meal plan recipe
INSERT OR REPLACE INTO recipes
INSERT INTO recipes
(id, name, type)
SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow'
FROM meal_plan
@ -77,9 +77,6 @@ BEGIN
AND type = 'recipe'
AND recipe_id IS NOT NULL;
DELETE FROM recipes_nestings
WHERE recipe_id IN (SELECT id FROM recipes WHERE name IN (SELECT CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT) FROM meal_plan WHERE day = NEW.day) AND type = 'mealplan-shadow');
INSERT INTO recipes_nestings
(recipe_id, includes_recipe_id, servings)
SELECT (SELECT id FROM recipes WHERE name = CAST(NEW.day AS TEXT) || '#' || CAST(meal_plan.id AS TEXT) AND type = 'mealplan-shadow'), recipe_id, recipe_servings
@ -190,7 +187,7 @@ BEGIN
*/
-- Create a shadow recipe per meal plan recipe
INSERT OR REPLACE INTO recipes
INSERT INTO recipes
(id, name, type)
SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow'
FROM meal_plan
@ -292,7 +289,14 @@ BEGIN
GROUP BY product_id, product_qu_id;
-- Create a shadow recipe per meal plan recipe
INSERT OR REPLACE INTO recipes
DELETE FROM recipes_nestings
WHERE recipe_id IN (SELECT id FROM recipes WHERE name IN (SELECT CAST(NEW.day AS TEXT) || '#' || CAST(NEW.id AS TEXT) FROM meal_plan WHERE day = NEW.day) AND type = 'mealplan-shadow');
DELETE FROM recipes
WHERE type = 'mealplan-shadow'
AND name = CAST(NEW.day AS TEXT) || '#' || CAST(NEW.id AS TEXT);
INSERT INTO recipes
(id, name, type)
SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow'
FROM meal_plan
@ -300,9 +304,6 @@ BEGIN
AND type = 'recipe'
AND recipe_id IS NOT NULL;
DELETE FROM recipes_nestings
WHERE recipe_id IN (SELECT id FROM recipes WHERE name IN (SELECT CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT) FROM meal_plan WHERE day = NEW.day) AND type = 'mealplan-shadow');
INSERT INTO recipes_nestings
(recipe_id, includes_recipe_id, servings)
SELECT (SELECT id FROM recipes WHERE name = CAST(NEW.day AS TEXT) || '#' || CAST(meal_plan.id AS TEXT) AND type = 'mealplan-shadow'), recipe_id, recipe_servings

14
migrations/0149.sql Normal file
View File

@ -0,0 +1,14 @@
CREATE TABLE meal_plan_sections (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
sort_number INTEGER,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
INSERT INTO meal_plan_sections
(id, name, sort_number)
VALUES
(-1, '', -1);
ALTER TABLE meal_plan
ADD section_id INTEGER NOT NULL DEFAULT -1;

View File

@ -1116,3 +1116,5 @@ $(document).on("click", ".change-table-columns-rowgroup-toggle", function()
dataTable.draw();
});
$("#meal-plan-nav-link").attr("href", $("#meal-plan-nav-link").attr("href") + "?week=" + moment().startOf("week").format("YYYY-MM-DD"));

View File

@ -12,23 +12,50 @@ if (!Grocy.MealPlanFirstDayOfWeek.isEmpty())
firstDay = parseInt(Grocy.MealPlanFirstDayOfWeek);
}
var calendar = $("#calendar").fullCalendar({
"themeSystem": "bootstrap4",
"header": {
$(".calendar").each(function()
{
var container = $(this);
var sectionId = container.attr("data-section-id");
var sectionName = container.attr("data-section-name");
var isPrimarySection = BoolVal(container.attr("data-primary-section"));
var isLastSection = BoolVal(container.attr("data-last-section"));
var headerConfig = {
"left": "title",
"center": "",
"right": "prev,today,next"
},
"weekNumbers": false,
"eventLimit": false,
"eventSources": fullcalendarEventSources,
"defaultView": ($(window).width() < 768) ? "basicDay" : "basicWeek",
"firstDay": firstDay,
"height": "auto",
"defaultDate": GetUriParam("week"),
"viewRender": function(view)
};
if (!isPrimarySection)
{
$(".fc-day-header").prepend('\
headerConfig = {
"left": "",
"center": "",
"right": ""
};
}
container.fullCalendar({
"themeSystem": "bootstrap4",
"header": headerConfig,
"weekNumbers": false,
"eventLimit": false,
"eventSources": fullcalendarEventSources,
"defaultView": ($(window).width() < 768) ? "agendaDay" : "agendaWeek",
"allDayText": sectionName,
"minTime": "00:00:00",
"maxTime": "00:00:01",
"scrollTime": "00:00:00",
"firstDay": firstDay,
"height": "auto",
"defaultDate": GetUriParam("week"),
"viewRender": function(view)
{
if (!isPrimarySection)
{
return;
}
$(".calendar[data-primary-section='true'] .fc-day-header").prepend('\
<div class="btn-group mr-2 my-1"> \
<button type="button" class="btn btn-outline-dark btn-xs add-recipe-button" data-toggle="tooltip" title="' + __t('Add recipe') + '"><i class="fas fa-plus"></i></a></button> \
<button type="button" class="btn btn-outline-dark btn-xs dropdown-toggle dropdown-toggle-split" data-toggle="dropdown"></button> \
@ -39,114 +66,119 @@ var calendar = $("#calendar").fullCalendar({
</div> \
</div>');
var weekRecipeName = view.start.year().toString() + "-" + ((view.start.week() - 1).toString().padStart(2, "0")).toString();
var weekRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", weekRecipeName);
var weekCosts = 0;
var weekRecipeOrderMissingButtonHtml = "";
var weekRecipeConsumeButtonHtml = "";
var weekCostsHtml = "";
if (weekRecipe !== null)
{
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs;
weekCostsHtml = __t("Week costs") + ': <span class="locale-number locale-number-currency">' + weekCosts.toString() + "</span> ";
}
var weekRecipeOrderMissingButtonDisabledClasses = "";
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled_with_shopping_list == 1)
{
weekRecipeOrderMissingButtonDisabledClasses = "disabled";
}
var weekRecipeConsumeButtonDisabledClasses = "";
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0 || weekCosts == 0)
{
weekRecipeConsumeButtonDisabledClasses = "disabled";
}
var weekRecipeName = view.start.year().toString() + "-" + ((view.start.week() - 1).toString().padStart(2, "0")).toString();
var weekRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", weekRecipeName);
var weekCosts = 0;
var weekRecipeOrderMissingButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
var weekRecipeConsumeButtonHtml = "";
var weekCostsHtml = "";
if (weekRecipe !== null)
{
weekRecipeOrderMissingButtonHtml = '<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + weekRecipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-cart-plus"></i></a>';
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs;
weekCostsHtml = __t("Week costs") + ': <span class="locale-number locale-number-currency">' + weekCosts.toString() + "</span> ";
}
var weekRecipeOrderMissingButtonDisabledClasses = "";
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled_with_shopping_list == 1)
{
weekRecipeOrderMissingButtonDisabledClasses = "disabled";
}
var weekRecipeConsumeButtonDisabledClasses = "";
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0 || weekCosts == 0)
{
weekRecipeConsumeButtonDisabledClasses = "disabled";
}
var weekRecipeOrderMissingButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
{
weekRecipeOrderMissingButtonHtml = '<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + weekRecipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-cart-plus"></i></a>';
}
weekRecipeConsumeButtonHtml = '<a class="ml-1 btn btn-outline-success btn-xs recipe-consume-button ' + weekRecipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this weeks recipes or products") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-utensils"></i></a>'
}
weekRecipeConsumeButtonHtml = '<a class="ml-1 btn btn-outline-success btn-xs recipe-consume-button ' + weekRecipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this weeks recipes or products") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-utensils"></i></a>'
}
$(".fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
},
"eventRender": function(event, element)
{
element.removeClass("fc-event");
element.addClass("text-center");
element.attr("data-meal-plan-entry", event.mealPlanEntry);
var mealPlanEntry = JSON.parse(event.mealPlanEntry);
var additionalTitleCssClasses = "";
var doneButtonHtml = '<a class="ml-1 btn btn-outline-secondary btn-xs mealplan-entry-done-button" href="#" data-toggle="tooltip" title="' + __t("Mark this item as done") + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '"><i class="fas fa-check"></i></a>';
if (BoolVal(mealPlanEntry.done))
$(".calendar[data-primary-section='true'] .fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
},
"eventRender": function(event, element)
{
additionalTitleCssClasses = "text-strike-through text-muted";
doneButtonHtml = '<a class="ml-1 btn btn-outline-secondary btn-xs mealplan-entry-undone-button" href="#" data-toggle="tooltip" title="' + __t("Mark this item as undone") + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '"><i class="fas fa-undo"></i></a>';
}
element.removeClass("fc-event");
element.addClass("text-center");
element.attr("data-meal-plan-entry", event.mealPlanEntry);
if (event.type == "recipe")
{
var recipe = JSON.parse(event.recipe);
if (recipe === null || recipe === undefined)
var mealPlanEntry = JSON.parse(event.mealPlanEntry);
if (sectionId != mealPlanEntry.section_id)
{
return false;
}
var internalShadowRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", mealPlanEntry.day + "#" + mealPlanEntry.id);
var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", internalShadowRecipe.id);
element.attr("data-recipe", event.recipe);
var recipeOrderMissingButtonDisabledClasses = "";
if (resolvedRecipe.need_fulfilled_with_shopping_list == 1)
var additionalTitleCssClasses = "";
var doneButtonHtml = '<a class="ml-1 btn btn-outline-secondary btn-xs mealplan-entry-done-button" href="#" data-toggle="tooltip" title="' + __t("Mark this item as done") + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '"><i class="fas fa-check"></i></a>';
if (BoolVal(mealPlanEntry.done))
{
recipeOrderMissingButtonDisabledClasses = "disabled";
additionalTitleCssClasses = "text-strike-through text-muted";
doneButtonHtml = '<a class="ml-1 btn btn-outline-secondary btn-xs mealplan-entry-undone-button" href="#" data-toggle="tooltip" title="' + __t("Mark this item as undone") + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '"><i class="fas fa-undo"></i></a>';
}
var recipeConsumeButtonDisabledClasses = "";
if (resolvedRecipe.need_fulfilled == 0)
if (event.type == "recipe")
{
recipeConsumeButtonDisabledClasses = "disabled";
}
var recipe = JSON.parse(event.recipe);
if (recipe === null || recipe === undefined)
{
return false;
}
var fulfillmentInfoHtml = __t('Enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>';
if (resolvedRecipe.need_fulfilled != 1)
{
fulfillmentInfoHtml = __t('Not enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>';
}
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-currency">' + resolvedRecipe.costs + '</span> / <span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '</h5>';
}
else
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '</h5>';
}
var internalShadowRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", mealPlanEntry.day + "#" + mealPlanEntry.id);
var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", internalShadowRecipe.id);
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK)
{
fulfillmentIconHtml = "";
fulfillmentInfoHtml = "";
}
element.attr("data-recipe", event.recipe);
var shoppingListButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
{
shoppingListButtonHtml = '<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + recipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + recipe.id.toString() + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-cart-plus"></i></a>';
}
var recipeOrderMissingButtonDisabledClasses = "";
if (resolvedRecipe.need_fulfilled_with_shopping_list == 1)
{
recipeOrderMissingButtonDisabledClasses = "disabled";
}
element.html('\
var recipeConsumeButtonDisabledClasses = "";
if (resolvedRecipe.need_fulfilled == 0)
{
recipeConsumeButtonDisabledClasses = "disabled";
}
var fulfillmentInfoHtml = __t('Enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>';
if (resolvedRecipe.need_fulfilled != 1)
{
fulfillmentInfoHtml = __t('Not enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>';
}
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-currency">' + resolvedRecipe.costs + '</span> / <span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '</h5>';
}
else
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '</h5>';
}
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK)
{
fulfillmentIconHtml = "";
fulfillmentInfoHtml = "";
}
var shoppingListButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
{
shoppingListButtonHtml = '<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + recipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + recipe.id.toString() + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-cart-plus"></i></a>';
}
element.html('\
<div> \
<h5 class="text-truncate mb-1 cursor-link display-recipe-button ' + additionalTitleCssClasses + '" data-toggle="tooltip" title="' + __t("Display recipe") + '" data-recipe-id="' + recipe.id.toString() + '" data-recipe-name="' + recipe.name + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-type="' + recipe.type + '">' + recipe.name + '</h5> \
<h5 class="small text-truncate mb-1">' + __n(mealPlanEntry.recipe_servings, "%s serving", "%s servings") + '</h5> \
@ -161,84 +193,63 @@ var calendar = $("#calendar").fullCalendar({
</h5> \
</div>');
if (recipe.picture_file_name && !recipe.picture_file_name.isEmpty())
{
element.prepend('<div class="mx-auto mb-1"><img data-src="' + U("/api/files/recipepictures/") + btoa(recipe.picture_file_name) + '?force_serve_as=picture&best_fit_width=400" class="img-fluid rounded-circle lazy"></div>')
}
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)
if (recipe.picture_file_name && !recipe.picture_file_name.isEmpty())
{
var dayRecipeResolved = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", dayRecipe.id);
var costsAndCaloriesPerDay = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + dayRecipeResolved.costs + '</span> / <span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
}
else
{
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
}
$(".fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>');
element.prepend('<div class="mx-auto mb-1"><img data-src="' + U("/api/files/recipepictures/") + btoa(recipe.picture_file_name) + '?force_serve_as=picture&best_fit_width=400" class="img-fluid rounded-circle lazy"></div>')
}
}
}
if (event.type == "product")
{
var productDetails = JSON.parse(event.productDetails);
if (productDetails === null || productDetails === undefined)
else if (event.type == "product")
{
return false;
}
var productDetails = JSON.parse(event.productDetails);
if (productDetails === null || productDetails === undefined)
{
return false;
}
if (productDetails.last_price === null)
{
productDetails.last_price = 0;
}
if (productDetails.last_price === null)
{
productDetails.last_price = 0;
}
element.attr("data-product-details", event.productDetails);
element.attr("data-product-details", event.productDetails);
var productOrderMissingButtonDisabledClasses = "disabled";
if (parseFloat(productDetails.stock_amount_aggregated) < parseFloat(mealPlanEntry.product_amount))
{
productOrderMissingButtonDisabledClasses = "";
}
var productOrderMissingButtonDisabledClasses = "disabled";
if (parseFloat(productDetails.stock_amount_aggregated) < parseFloat(mealPlanEntry.product_amount))
{
productOrderMissingButtonDisabledClasses = "";
}
var productConsumeButtonDisabledClasses = "disabled";
if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount))
{
productConsumeButtonDisabledClasses = "";
}
var productConsumeButtonDisabledClasses = "disabled";
if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount))
{
productConsumeButtonDisabledClasses = "";
}
fulfillmentInfoHtml = __t('Not enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>';
if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount))
{
var fulfillmentInfoHtml = __t('Enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>';
}
fulfillmentInfoHtml = __t('Not enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>';
if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount))
{
var fulfillmentInfoHtml = __t('Enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>';
}
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-currency">' + productDetails.last_price * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '</h5>';
}
else
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '</h5>';
}
var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-currency">' + productDetails.last_price * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '</h5>';
}
else
{
costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '</h5>';
}
var shoppingListButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
{
shoppingListButtonHtml = '<a class="btn btn-outline-primary btn-xs show-as-dialog-link ' + productOrderMissingButtonDisabledClasses + '" href="' + U("/shoppinglistitem/new?embedded&updateexistingproduct&product=") + mealPlanEntry.product_id + '&amount=' + mealPlanEntry.product_amount + '" data-toggle="tooltip" title="' + __t("Add to shopping list") + '" data-product-id="' + productDetails.product.id.toString() + '" data-product-name="' + productDetails.product.name + '" data-product-amount="' + mealPlanEntry.product_amount + '"><i class="fas fa-cart-plus"></i></a>';
}
var shoppingListButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST)
{
shoppingListButtonHtml = '<a class="btn btn-outline-primary btn-xs show-as-dialog-link ' + productOrderMissingButtonDisabledClasses + '" href="' + U("/shoppinglistitem/new?embedded&updateexistingproduct&product=") + mealPlanEntry.product_id + '&amount=' + mealPlanEntry.product_amount + '" data-toggle="tooltip" title="' + __t("Add to shopping list") + '" data-product-id="' + productDetails.product.id.toString() + '" data-product-name="' + productDetails.product.name + '" data-product-amount="' + mealPlanEntry.product_amount + '"><i class="fas fa-cart-plus"></i></a>';
}
element.html('\
element.html('\
<div> \
<h5 class="text-truncate mb-1 cursor-link display-product-button ' + additionalTitleCssClasses + '" data-toggle="tooltip" title="' + __t("Display product") + '" data-product-id="' + productDetails.product.id.toString() + '">' + productDetails.product.name + '</h5> \
<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-quantity-amount">' + mealPlanEntry.product_amount + "</span> " + __n(mealPlanEntry.product_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural) + '</h5> \
@ -253,9 +264,22 @@ var calendar = $("#calendar").fullCalendar({
</h5> \
</div>');
if (productDetails.product.picture_file_name && !productDetails.product.picture_file_name.isEmpty())
if (productDetails.product.picture_file_name && !productDetails.product.picture_file_name.isEmpty())
{
element.prepend('<div class="mx-auto mb-1"><img data-src="' + U("/api/files/productpictures/") + btoa(productDetails.product.picture_file_name) + '?force_serve_as=picture&best_fit_width=400" class="img-fluid rounded-circle lazy"></div>')
}
}
else if (event.type == "note")
{
element.prepend('<div class="mx-auto mb-1"><img data-src="' + U("/api/files/productpictures/") + btoa(productDetails.product.picture_file_name) + '?force_serve_as=picture&best_fit_width=400" class="img-fluid rounded-circle lazy"></div>')
element.html('\
<div> \
<h5 class="text-wrap text-break mb-1 ' + additionalTitleCssClasses + '">' + mealPlanEntry.note + '</h5> \
<h5> \
<a class="ml-1 btn btn-outline-danger btn-xs remove-note-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
<a class="btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-edit"></i></a> \
' + doneButtonHtml + ' \
</h5> \
</div>');
}
var dayRecipeName = event.start.format("YYYY-MM-DD");
@ -275,47 +299,48 @@ var calendar = $("#calendar").fullCalendar({
{
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
}
$(".fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>');
$(".calendar[data-primary-section='true'] .fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>');
}
}
},
"eventAfterAllRender": function(view)
{
if (isPrimarySection)
{
UpdateUriParam("week", view.start.format("YYYY-MM-DD"));
if (firstRender)
{
firstRender = false
}
else
{
$(".calendar").addClass("d-none");
window.location.reload();
return false;
}
}
if (isLastSection)
{
$(".fc-axis span").replaceWith(function()
{
return $("<div />", { html: $(this).html() });
});
RefreshLocaleNumberDisplay();
LoadImagesLazy();
$('[data-toggle="tooltip"]').tooltip();
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK)
{
$(".recipe-order-missing-button").addClass("d-none");
$(".recipe-consume-button").addClass("d-none");
}
}
}
else if (event.type == "note")
{
element.html('\
<div> \
<h5 class="text-wrap text-break mb-1 ' + additionalTitleCssClasses + '">' + mealPlanEntry.note + '</h5> \
<h5> \
<a class="ml-1 btn btn-outline-danger btn-xs remove-note-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
<a class="btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-edit"></i></a> \
' + doneButtonHtml + ' \
</h5> \
</div>');
}
},
"eventAfterAllRender": function(view)
{
UpdateUriParam("week", view.start.format("YYYY-MM-DD"));
if (firstRender)
{
firstRender = false
}
else
{
window.location.reload();
return false;
}
RefreshLocaleNumberDisplay();
LoadImagesLazy();
$('[data-toggle="tooltip"]').tooltip();
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK)
{
$(".recipe-order-missing-button").addClass("d-none");
$(".recipe-consume-button").addClass("d-none");
}
},
});
});
$(document).on("click", ".add-recipe-button", function(e)
@ -325,6 +350,7 @@ $(document).on("click", ".add-recipe-button", function(e)
$("#add-recipe-modal-title").text(__t("Add recipe on %s", day.toString()));
$("#day").val(day.toString());
Grocy.Components.RecipePicker.Clear();
$("#section_id_note").val(-1);
$("#add-recipe-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-recipe-form");
Grocy.IsMealPlanEntryEditAction = false;
@ -337,6 +363,7 @@ $(document).on("click", ".add-note-button", function(e)
$("#add-note-modal-title").text(__t("Add note on %s", day.toString()));
$("#day").val(day.toString());
$("#note").val("");
$("#section_id_note").val(-1);
$("#add-note-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-note-form");
Grocy.IsMealPlanEntryEditAction = false;
@ -349,6 +376,7 @@ $(document).on("click", ".add-product-button", function(e)
$("#add-product-modal-title").text(__t("Add product on %s", day.toString()));
$("#day").val(day.toString());
Grocy.Components.ProductPicker.Clear();
$("#section_id_note").val(-1);
$("#add-product-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-product-form");
Grocy.IsMealPlanEntryEditAction = false;
@ -365,6 +393,7 @@ $(document).on("click", ".edit-meal-plan-entry-button", function(e)
$("#recipe_servings").val(mealPlanEntry.recipe_servings);
Grocy.Components.RecipePicker.SetId(mealPlanEntry.recipe_id);
$("#add-recipe-modal").modal("show");
$("#section_id_recipe").val(mealPlanEntry.section_id);
Grocy.FrontendHelpers.ValidateForm("add-recipe-form");
}
else if (mealPlanEntry.type == "product")
@ -373,6 +402,7 @@ $(document).on("click", ".edit-meal-plan-entry-button", function(e)
$("#day").val(mealPlanEntry.day.toString());
Grocy.Components.ProductPicker.SetId(mealPlanEntry.product_id);
$("#add-product-modal").modal("show");
$("#section_id_product").val(mealPlanEntry.section_id);
Grocy.FrontendHelpers.ValidateForm("add-product-form");
Grocy.Components.ProductPicker.GetPicker().trigger("change");
}
@ -382,6 +412,7 @@ $(document).on("click", ".edit-meal-plan-entry-button", function(e)
$("#day").val(mealPlanEntry.day.toString());
$("#note").val(mealPlanEntry.note);
$("#add-note-modal").modal("show");
$("#section_id_note").val(mealPlanEntry.section_id);
Grocy.FrontendHelpers.ValidateForm("add-note-form");
}
Grocy.IsMealPlanEntryEditAction = true;
@ -450,9 +481,13 @@ $('#save-add-recipe-button').on('click', function(e)
return false;
}
var formData = $('#add-recipe-form').serializeJSON();
formData.section_id = formData.section_id_recipe;
delete formData.section_id_recipe;
if (Grocy.IsMealPlanEntryEditAction)
{
Grocy.Api.Put('objects/meal_plan/' + Grocy.MealPlanEntryEditObjectId.toString(), $('#add-recipe-form').serializeJSON(),
Grocy.Api.Put('objects/meal_plan/' + Grocy.MealPlanEntryEditObjectId.toString(), formData,
function(result)
{
window.location.reload();
@ -465,7 +500,7 @@ $('#save-add-recipe-button').on('click', function(e)
}
else
{
Grocy.Api.Post('objects/meal_plan', $('#add-recipe-form').serializeJSON(),
Grocy.Api.Post('objects/meal_plan', formData,
function(result)
{
window.location.reload();
@ -494,6 +529,8 @@ $('#save-add-note-button').on('click', function(e)
var jsonData = $('#add-note-form').serializeJSON();
jsonData.day = $("#day").val();
jsonData.section_id = formData.section_id_note;
delete formData.section_id_note;
if (Grocy.IsMealPlanEntryEditAction)
{
@ -545,6 +582,8 @@ $('#save-add-product-button').on('click', function(e)
delete jsonData.amount;
jsonData.product_qu_id = $("#qu_id").val();;
delete jsonData.qu_id;
jsonData.section_id = jsonData.section_id_product;
delete jsonData.section_id_product;
if (Grocy.IsMealPlanEntryEditAction)
{
@ -939,16 +978,19 @@ $(document).on("click", ".mealplan-entry-undone-button", function(e)
$(window).one("resize", function()
{
// Automatically switch the calendar to "basicDay" view on small screens
// and to "basicWeek" otherwise
if ($(window).width() < 768)
// Automatically switch the calendar to "agendaDay" view on small screens and to "agendaWeek" otherwise
var windowWidth = $(window).width();
$(".calendar").each(function()
{
calendar.fullCalendar("changeView", "basicDay");
}
else
{
calendar.fullCalendar("changeView", "basicWeek");
}
if (windowWidth < 768)
{
$(this).fullCalendar("changeView", "agendaDay");
}
else
{
$(this).fullCalendar("changeView", "agendaWeek");
}
});
});
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)

View File

@ -0,0 +1,75 @@
$('#save-mealplansection-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#mealplansection-form').serializeJSON();
Grocy.FrontendHelpers.BeginUiBusy("mealplansection-form");
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('objects/meal_plan_sections', jsonData,
function(result)
{
if (GetUriParam("embedded") !== undefined)
{
window.parent.postMessage(WindowMessageBag("Reload"), Grocy.BaseUrl);
}
else
{
window.location.href = U('/mealplansections');
}
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("mealplansection-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Put('objects/meal_plan_sections/' + Grocy.EditObjectId, jsonData,
function(result)
{
if (GetUriParam("embedded") !== undefined)
{
window.parent.postMessage(WindowMessageBag("Reload"), Grocy.BaseUrl);
}
else
{
window.location.href = U('/mealplansections');
}
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("mealplansection-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#mealplansection-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('mealplansection-form');
});
$('#mealplansection-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('mealplansection-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-mealplansections-button').click();
}
}
});
Grocy.FrontendHelpers.ValidateForm('mealplansection-form');
$('#name').focus();

View File

@ -0,0 +1,63 @@
var mealplanSectionsTable = $('#mealplansections-table').DataTable({
'order': [[2, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#mealplansections-table tbody').removeClass("d-none");
mealplanSectionsTable.columns.adjust().draw();
$("#search").on("keyup", Delay(function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
mealplanSectionsTable.search(value).draw();
}, 200));
$("#clear-filter-button").on("click", function()
{
$("#search").val("");
mealplanSectionsTable.search("").draw();
});
$(document).on('click', '.mealplansection-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-mealplansection-name');
var objectId = $(e.currentTarget).attr('data-mealplansection-id');
bootbox.confirm({
message: __t('Are you sure to delete meal plan section "%s"?', objectName),
closeButton: false,
buttons: {
confirm: {
label: __t('Yes'),
className: 'btn-success'
},
cancel: {
label: __t('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Delete('objects/meal_plan_sections/' + objectId, {},
function(result)
{
window.location.href = U('/mealplansections');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});

View File

@ -87,6 +87,8 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->get('/recipe/{recipeId}', '\Grocy\Controllers\RecipesController:RecipeEditForm');
$group->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm');
$group->get('/mealplan', '\Grocy\Controllers\RecipesController:MealPlan');
$group->get('/mealplansections', '\Grocy\Controllers\RecipesController:MealPlanSectionsList');
$group->get('/mealplansection/{sectionId}', '\Grocy\Controllers\RecipesController:MealPlanSectionEditForm');
$group->get('/recipessettings', '\Grocy\Controllers\RecipesController:RecipesSettings');
}

View File

@ -140,15 +140,22 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO recipes_nestings(recipe_id, includes_recipe_id) VALUES (6, 4);
INSERT INTO recipes_nestings(recipe_id, includes_recipe_id) VALUES (6, 5);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$mondayThisWeek}', 1);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$tuesdayThisWeek}', 2);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$wednesdayThisWeek}', 3);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$thursdayThisWeek}', 4);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$fridayThisWeek}', 1);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$saturdayThisWeek}', 2);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$sundayThisWeek}', 4);
INSERT INTO meal_plan(day, type, note) VALUES ('{$tuesdayThisWeek}', 'note', '{$this->__t_sql('This is a note')}');
INSERT INTO meal_plan(day, type, product_id, product_amount) VALUES (DATE('{$mondayThisWeek}', '-1 days'), 'product', 3, 1);
INSERT INTO meal_plan_sections (name, sort_number) VALUES ('{$this->__t_sql('Breakfast')}', 10);
INSERT INTO meal_plan_sections (name, sort_number) VALUES ('{$this->__t_sql('Lunch')}', 20);
INSERT INTO meal_plan_sections (name, sort_number) VALUES ('{$this->__t_sql('Dinner')}', 30);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$mondayThisWeek}', 1, 2);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$tuesdayThisWeek}', 2, 2);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$wednesdayThisWeek}', 3, 3);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$thursdayThisWeek}', 4, 1);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$fridayThisWeek}', 2, 2);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$saturdayThisWeek}', 1, 2);
INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$sundayThisWeek}', 4, 2);
INSERT INTO meal_plan(day, type, note, section_id) VALUES ('{$tuesdayThisWeek}', 'note', '{$this->__t_sql('This is a note')}', 1);
INSERT INTO meal_plan(day, type, product_id, product_amount, section_id) VALUES (DATE('{$mondayThisWeek}', '-1 days'), 'product', 3, 1, 3);
INSERT INTO meal_plan(day, type, product_id, product_amount, section_id) VALUES (DATE('{$tuesdayThisWeek}', '-1 days'), 'product', 9, 1, 1);
INSERT INTO meal_plan(day, type, product_id, product_amount, section_id) VALUES (DATE('{$thursdayThisWeek}', '-1 days'), 'product', 25, 1, 1);
INSERT INTO meal_plan(day, type, note, section_id) VALUES ('{$saturdayThisWeek}', 'note', '{$this->__t_sql('Some good snacks')}', 3);
INSERT INTO chores (name, period_type, period_days) VALUES ('{$this->__t_sql('Changed towels in the bathroom')}', 'manually', 5); --1
INSERT INTO chores (name, period_type, period_days, assignment_type, assignment_config, next_execution_assigned_to_user_id) VALUES ('{$this->__t_sql('Cleaned the kitchen floor')}', 'dynamic-regular', 7, 'random', '1,2,3,4', 1); --2

View File

@ -188,7 +188,8 @@
data-placement="right"
title="{{ $__t('Meal plan') }}"
data-nav-for-page="mealplan">
<a class="nav-link discrete-link"
<a id="meal-plan-nav-link"
class="nav-link discrete-link"
href="{{ $U('/mealplan') }}">
<i class="fas fa-paper-plane"></i>
<span class="nav-link-text">{{ $__t('Meal plan') }}</span>

View File

@ -24,6 +24,39 @@
max-height: 140px;
}
.fc-time-grid-container,
hr.fc-divider {
display: none;
}
.fc-axis {
width: 25px !important;
}
.fc-axis div {
transform: translateX(-50%) translateY(-50%) rotate(-90deg);
font-weight: bold;
font-size: 1.8em;
letter-spacing: 0.1em;
position: absolute;
top: 50%;
left: 0;
margin-left: 15px;
}
.fc-content-skeleton {
padding-bottom: 0 !important;
}
.calendar[data-primary-section='false'] .fc-toolbar.fc-header-toolbar,
.calendar[data-primary-section='false'] .fc-head {
display: none;
}
.calendar[data-primary-section='false'] {
border-top: #d6d6d6 solid 5px;
}
@media (min-width: 400px) {
.table-inline-menu.dropdown-menu {
width: 200px !important;
@ -47,17 +80,56 @@
<div class="row">
<div class="col">
<h2 class="title">@yield('title')</h2>
<div class="title-related-links">
<h2 class="title">@yield('title')</h2>
<div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3"
type="button"
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i>
</button>
</div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100"
id="related-links">
<a class="btn btn-outline-secondary m-1 mt-md-0 mb-md-0 float-right"
href="{{ $U('/mealplansections') }}">
{{ $__t('Configure sections') }}
</a>
</div>
</div>
</div>
</div>
<hr class="my-2">
@foreach($usedMealplanSections as $mealplanSection)
<div class="row">
<div class="col">
<div id="calendar"></div>
<div class="calendar"
data-section-id="{{ $mealplanSection->id }}"
data-section-name="{{ $mealplanSection->name }}"
data-primary-section="{{ BoolToString($loop->first) }}"
{{-- $loop->last doesn't work however, is always null... --}}
data-last-section="{{ BoolToString(array_values(array_slice($usedMealplanSections->fetchAll(), -1))[0]->id == $mealplanSection->id) }}">
</div>
</div>
</div>
@endforeach
{{-- Default empty calendar/section when no single meal plan entry is in the given date range --}}
@if($usedMealplanSections->count() === 0)
<div class="row">
<div class="col">
<div class="calendar"
data-section-id="-1"
data-section-name=""
data-primary-section="true"
data-last-section="true">
</div>
</div>
</div>
@endif
<div class="modal fade"
id="add-recipe-modal"
@ -87,6 +159,18 @@
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount'
))
<div class="form-group">
<label for="period_type">{{ $__t('Section') }}</label>
<select class="custom-control custom-select"
id="section_id_recipe"
name="section_id_recipe"
required>
@foreach($mealplanSections as $mealplanSection)
<option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option>
@endforeach
</select>
</div>
<input type="hidden"
id="day"
name="day"
@ -130,6 +214,18 @@
name="note"></textarea>
</div>
<div class="form-group">
<label for="period_type">{{ $__t('Section') }}</label>
<select class="custom-control custom-select"
id="section_id_note"
name="section_id_note"
required>
@foreach($mealplanSections as $mealplanSection)
<option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option>
@endforeach
</select>
</div>
<input type="hidden"
name="type"
value="note">
@ -171,6 +267,18 @@
'additionalGroupCssClasses' => 'mb-0'
))
<div class="form-group">
<label for="period_type">{{ $__t('Section') }}</label>
<select class="custom-control custom-select"
id="section_id_product"
name="section_id_product"
required>
@foreach($mealplanSections as $mealplanSection)
<option value="{{ $mealplanSection->id }}">{{ $mealplanSection->name }}</option>
@endforeach
</select>
</div>
<input type="hidden"
name="type"
value="product">

View File

@ -0,0 +1,62 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $__t('Edit meal plan section'))
@else
@section('title', $__t('Create meal plan section'))
@endif
@section('viewJsName', 'mealplansectionform')
@section('content')
<div class="row">
<div class="col">
<h2 class="title">@yield('title')</h2>
</div>
</div>
<hr class="my-2">
<div class="row">
<div class="col-lg-6 col-12">
<script>
Grocy.EditMode = '{{ $mode }}';
</script>
@if($mode == 'edit')
<script>
Grocy.EditObjectId = {{ $mealplanSection->id }};
</script>
@endif
<form id="mealplansection-form"
novalidate>
<div class="form-group">
<label for="name">{{ $__t('Name') }}</label>
<input type="text"
class="form-control"
required
id="name"
name="name"
value="@if($mode == 'edit'){{ $mealplanSection->name }}@endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
@php if($mode == 'edit' && !empty($mealplanSection->sort_number)) { $value = $mealplanSection->sort_number; } else { $value = ''; } @endphp
@include('components.numberpicker', array(
'id' => 'sort_number',
'label' => 'Sort number',
'min' => 0,
'value' => $value,
'isRequired' => false,
'hint' => $__t('Sections will be ordered by that number on the meal plan')
))
<button id="save-mealplansection-button"
class="btn btn-success">{{ $__t('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@ -0,0 +1,111 @@
@extends('layout.default')
@section('title', $__t('Meal plan sections'))
@section('activeNav', 'mealplansections')
@section('viewJsName', 'mealplansections')
@section('content')
<div class="row">
<div class="col">
<div class="title-related-links">
<h2 class="title">@yield('title')</h2>
<div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3"
type="button"
data-toggle="collapse"
data-target="#table-filter-row">
<i class="fas fa-filter"></i>
</button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3"
type="button"
data-toggle="collapse"
data-target="#related-links">
<i class="fas fa-ellipsis-v"></i>
</button>
</div>
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100"
id="related-links">
<a class="btn btn-primary responsive-button m-1 mt-md-0 mb-md-0 float-right show-as-dialog-link"
href="{{ $U('/mealplansection/new?embedded') }}">
{{ $__t('Add') }}
</a>
</div>
</div>
</div>
</div>
<hr class="my-2">
<div class="row collapse d-md-flex"
id="table-filter-row">
<div class="col-12 col-md-6 col-xl-3">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-search"></i></span>
</div>
<input type="text"
id="search"
class="form-control"
placeholder="{{ $__t('Search') }}">
</div>
</div>
<div class="col">
<div class="float-right">
<a id="clear-filter-button"
class="btn btn-sm btn-outline-info"
href="#">
{{ $__t('Clear filter') }}
</a>
</div>
</div>
</div>
<div class="row">
<div class="col">
<table id="mealplansections-table"
class="table table-sm table-striped nowrap w-100">
<thead>
<tr>
<th class="border-right"><a class="text-muted change-table-columns-visibility-button"
data-toggle="tooltip"
data-toggle="tooltip"
title="{{ $__t('Table options') }}"
data-table-selector="#mealplansections-table"
href="#"><i class="fas fa-eye"></i></a>
</th>
<th>{{ $__t('Name') }}</th>
<th>{{ $__t('Sort number') }}</th>
</tr>
</thead>
<tbody class="d-none">
@foreach($mealplanSections as $mealplanSection)
<tr>
<td class="fit-content border-right">
<a class="btn btn-info btn-sm show-as-dialog-link"
href="{{ $U('/mealplansection/') }}{{ $mealplanSection->id }}?embedded"
data-toggle="tooltip"
title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm mealplansection-delete-button"
href="#"
data-mealplansection-id="{{ $mealplanSection->id }}"
data-mealplansection-name="{{ $mealplanSection->name }}"
data-toggle="tooltip"
title="{{ $__t('Delete this item') }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $mealplanSection->name }}
</td>
<td>
{{ $mealplanSection->sort_number }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop