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). > ⚠️ 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 - 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 - Can be printed (or downloaded) via
- The product/chore/battery edit page - The product/chore/battery edit page
@ -19,6 +20,11 @@
- https://github.com/grocy/grocy/blob/master/docs/label-printing.md - https://github.com/grocy/grocy/blob/master/docs/label-printing.md
- (Thanks a lot @mistressofjellyfish for the initial work on this) - (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 ### New feature: Shopping list thermal printer support
- The shopping list can now be printed on a thermal printer - 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) - 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) 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'])) if (isset($request->getQueryParams()['week']) && IsIsoDate($request->getQueryParams()['week']))
{ {
$week = $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 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(); $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)"), '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'), 'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->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'); 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) public function __construct(\DI\Container $container)
{ {
parent::__construct($container); parent::__construct($container);

View File

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

View File

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

View File

@ -2223,3 +2223,24 @@ msgstr ""
msgid "Stock entry" msgid "Stock entry"
msgstr "" 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; GROUP BY product_id, product_qu_id;
-- Create a shadow recipe per meal plan recipe -- Create a shadow recipe per meal plan recipe
INSERT OR REPLACE INTO recipes INSERT INTO recipes
(id, name, type) (id, name, type)
SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow' SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow'
FROM meal_plan FROM meal_plan
@ -77,9 +77,6 @@ BEGIN
AND type = 'recipe' AND type = 'recipe'
AND recipe_id IS NOT NULL; 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 INSERT INTO recipes_nestings
(recipe_id, includes_recipe_id, servings) (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 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 -- Create a shadow recipe per meal plan recipe
INSERT OR REPLACE INTO recipes INSERT INTO recipes
(id, name, type) (id, name, type)
SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow' SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow'
FROM meal_plan FROM meal_plan
@ -292,7 +289,14 @@ BEGIN
GROUP BY product_id, product_qu_id; GROUP BY product_id, product_qu_id;
-- Create a shadow recipe per meal plan recipe -- 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) (id, name, type)
SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow' SELECT (SELECT MIN(id) - 1 FROM recipes), CAST(NEW.day AS TEXT) || '#' || CAST(id AS TEXT), 'mealplan-shadow'
FROM meal_plan FROM meal_plan
@ -300,9 +304,6 @@ BEGIN
AND type = 'recipe' AND type = 'recipe'
AND recipe_id IS NOT NULL; 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 INSERT INTO recipes_nestings
(recipe_id, includes_recipe_id, servings) (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 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(); 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); firstDay = parseInt(Grocy.MealPlanFirstDayOfWeek);
} }
var calendar = $("#calendar").fullCalendar({ $(".calendar").each(function()
"themeSystem": "bootstrap4", {
"header": { 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", "left": "title",
"center": "", "center": "",
"right": "prev,today,next" "right": "prev,today,next"
}, };
"weekNumbers": false, if (!isPrimarySection)
"eventLimit": false,
"eventSources": fullcalendarEventSources,
"defaultView": ($(window).width() < 768) ? "basicDay" : "basicWeek",
"firstDay": firstDay,
"height": "auto",
"defaultDate": GetUriParam("week"),
"viewRender": function(view)
{ {
$(".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"> \ <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 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> \ <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> \
</div>'); </div>');
var weekRecipeName = view.start.year().toString() + "-" + ((view.start.week() - 1).toString().padStart(2, "0")).toString(); var weekRecipeName = view.start.year().toString() + "-" + ((view.start.week() - 1).toString().padStart(2, "0")).toString();
var weekRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", weekRecipeName); 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 weekCosts = 0;
var weekRecipeOrderMissingButtonHtml = ""; 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>'
} }
$(".calendar[data-primary-section='true'] .fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
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>' },
} "eventRender": function(event, element)
$(".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))
{ {
additionalTitleCssClasses = "text-strike-through text-muted"; element.removeClass("fc-event");
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.addClass("text-center");
} element.attr("data-meal-plan-entry", event.mealPlanEntry);
if (event.type == "recipe") var mealPlanEntry = JSON.parse(event.mealPlanEntry);
{
var recipe = JSON.parse(event.recipe); if (sectionId != mealPlanEntry.section_id)
if (recipe === null || recipe === undefined)
{ {
return false; return false;
} }
var internalShadowRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", mealPlanEntry.day + "#" + mealPlanEntry.id); var additionalTitleCssClasses = "";
var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", internalShadowRecipe.id); 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))
element.attr("data-recipe", event.recipe);
var recipeOrderMissingButtonDisabledClasses = "";
if (resolvedRecipe.need_fulfilled_with_shopping_list == 1)
{ {
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 (event.type == "recipe")
if (resolvedRecipe.need_fulfilled == 0)
{ {
recipeConsumeButtonDisabledClasses = "disabled"; var recipe = JSON.parse(event.recipe);
} if (recipe === null || recipe === undefined)
{
return false;
}
var fulfillmentInfoHtml = __t('Enough in stock'); var internalShadowRecipe = FindObjectInArrayByPropertyValue(internalRecipes, "name", mealPlanEntry.day + "#" + mealPlanEntry.id);
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>'; var resolvedRecipe = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", internalShadowRecipe.id);
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) element.attr("data-recipe", event.recipe);
{
fulfillmentIconHtml = "";
fulfillmentInfoHtml = "";
}
var shoppingListButtonHtml = ""; var recipeOrderMissingButtonDisabledClasses = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST) if (resolvedRecipe.need_fulfilled_with_shopping_list == 1)
{ {
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>'; 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> \ <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="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> \ <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> \ </h5> \
</div>'); </div>');
if (recipe.picture_file_name && !recipe.picture_file_name.isEmpty()) 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)
{ {
var dayRecipeResolved = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", dayRecipe.id); 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 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>');
} }
} }
} else if (event.type == "product")
if (event.type == "product")
{
var productDetails = JSON.parse(event.productDetails);
if (productDetails === null || productDetails === undefined)
{ {
return false; var productDetails = JSON.parse(event.productDetails);
} if (productDetails === null || productDetails === undefined)
{
return false;
}
if (productDetails.last_price === null) if (productDetails.last_price === null)
{ {
productDetails.last_price = 0; productDetails.last_price = 0;
} }
element.attr("data-product-details", event.productDetails); element.attr("data-product-details", event.productDetails);
var productOrderMissingButtonDisabledClasses = "disabled"; var productOrderMissingButtonDisabledClasses = "disabled";
if (parseFloat(productDetails.stock_amount_aggregated) < parseFloat(mealPlanEntry.product_amount)) if (parseFloat(productDetails.stock_amount_aggregated) < parseFloat(mealPlanEntry.product_amount))
{ {
productOrderMissingButtonDisabledClasses = ""; productOrderMissingButtonDisabledClasses = "";
} }
var productConsumeButtonDisabledClasses = "disabled"; var productConsumeButtonDisabledClasses = "disabled";
if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount)) if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount))
{ {
productConsumeButtonDisabledClasses = ""; productConsumeButtonDisabledClasses = "";
} }
fulfillmentInfoHtml = __t('Not enough in stock'); fulfillmentInfoHtml = __t('Not enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>'; var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>';
if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount)) if (parseFloat(productDetails.stock_amount_aggregated) >= parseFloat(mealPlanEntry.product_amount))
{ {
var fulfillmentInfoHtml = __t('Enough in stock'); var fulfillmentInfoHtml = __t('Enough in stock');
var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>'; var fulfillmentIconHtml = '<i class="fas fa-check text-success"></i>';
} }
var costsAndCaloriesPerServing = "" var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) 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>'; 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 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>'; 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 = ""; var shoppingListButtonHtml = "";
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_SHOPPINGLIST) 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>'; 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> \ <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="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> \ <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> \ </h5> \
</div>'); </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"); 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>'; 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) $(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())); $("#add-recipe-modal-title").text(__t("Add recipe on %s", day.toString()));
$("#day").val(day.toString()); $("#day").val(day.toString());
Grocy.Components.RecipePicker.Clear(); Grocy.Components.RecipePicker.Clear();
$("#section_id_note").val(-1);
$("#add-recipe-modal").modal("show"); $("#add-recipe-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-recipe-form"); Grocy.FrontendHelpers.ValidateForm("add-recipe-form");
Grocy.IsMealPlanEntryEditAction = false; 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())); $("#add-note-modal-title").text(__t("Add note on %s", day.toString()));
$("#day").val(day.toString()); $("#day").val(day.toString());
$("#note").val(""); $("#note").val("");
$("#section_id_note").val(-1);
$("#add-note-modal").modal("show"); $("#add-note-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-note-form"); Grocy.FrontendHelpers.ValidateForm("add-note-form");
Grocy.IsMealPlanEntryEditAction = false; 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())); $("#add-product-modal-title").text(__t("Add product on %s", day.toString()));
$("#day").val(day.toString()); $("#day").val(day.toString());
Grocy.Components.ProductPicker.Clear(); Grocy.Components.ProductPicker.Clear();
$("#section_id_note").val(-1);
$("#add-product-modal").modal("show"); $("#add-product-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-product-form"); Grocy.FrontendHelpers.ValidateForm("add-product-form");
Grocy.IsMealPlanEntryEditAction = false; Grocy.IsMealPlanEntryEditAction = false;
@ -365,6 +393,7 @@ $(document).on("click", ".edit-meal-plan-entry-button", function(e)
$("#recipe_servings").val(mealPlanEntry.recipe_servings); $("#recipe_servings").val(mealPlanEntry.recipe_servings);
Grocy.Components.RecipePicker.SetId(mealPlanEntry.recipe_id); Grocy.Components.RecipePicker.SetId(mealPlanEntry.recipe_id);
$("#add-recipe-modal").modal("show"); $("#add-recipe-modal").modal("show");
$("#section_id_recipe").val(mealPlanEntry.section_id);
Grocy.FrontendHelpers.ValidateForm("add-recipe-form"); Grocy.FrontendHelpers.ValidateForm("add-recipe-form");
} }
else if (mealPlanEntry.type == "product") else if (mealPlanEntry.type == "product")
@ -373,6 +402,7 @@ $(document).on("click", ".edit-meal-plan-entry-button", function(e)
$("#day").val(mealPlanEntry.day.toString()); $("#day").val(mealPlanEntry.day.toString());
Grocy.Components.ProductPicker.SetId(mealPlanEntry.product_id); Grocy.Components.ProductPicker.SetId(mealPlanEntry.product_id);
$("#add-product-modal").modal("show"); $("#add-product-modal").modal("show");
$("#section_id_product").val(mealPlanEntry.section_id);
Grocy.FrontendHelpers.ValidateForm("add-product-form"); Grocy.FrontendHelpers.ValidateForm("add-product-form");
Grocy.Components.ProductPicker.GetPicker().trigger("change"); 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()); $("#day").val(mealPlanEntry.day.toString());
$("#note").val(mealPlanEntry.note); $("#note").val(mealPlanEntry.note);
$("#add-note-modal").modal("show"); $("#add-note-modal").modal("show");
$("#section_id_note").val(mealPlanEntry.section_id);
Grocy.FrontendHelpers.ValidateForm("add-note-form"); Grocy.FrontendHelpers.ValidateForm("add-note-form");
} }
Grocy.IsMealPlanEntryEditAction = true; Grocy.IsMealPlanEntryEditAction = true;
@ -450,9 +481,13 @@ $('#save-add-recipe-button').on('click', function(e)
return false; return false;
} }
var formData = $('#add-recipe-form').serializeJSON();
formData.section_id = formData.section_id_recipe;
delete formData.section_id_recipe;
if (Grocy.IsMealPlanEntryEditAction) 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) function(result)
{ {
window.location.reload(); window.location.reload();
@ -465,7 +500,7 @@ $('#save-add-recipe-button').on('click', function(e)
} }
else else
{ {
Grocy.Api.Post('objects/meal_plan', $('#add-recipe-form').serializeJSON(), Grocy.Api.Post('objects/meal_plan', formData,
function(result) function(result)
{ {
window.location.reload(); window.location.reload();
@ -494,6 +529,8 @@ $('#save-add-note-button').on('click', function(e)
var jsonData = $('#add-note-form').serializeJSON(); var jsonData = $('#add-note-form').serializeJSON();
jsonData.day = $("#day").val(); jsonData.day = $("#day").val();
jsonData.section_id = formData.section_id_note;
delete formData.section_id_note;
if (Grocy.IsMealPlanEntryEditAction) if (Grocy.IsMealPlanEntryEditAction)
{ {
@ -545,6 +582,8 @@ $('#save-add-product-button').on('click', function(e)
delete jsonData.amount; delete jsonData.amount;
jsonData.product_qu_id = $("#qu_id").val();; jsonData.product_qu_id = $("#qu_id").val();;
delete jsonData.qu_id; delete jsonData.qu_id;
jsonData.section_id = jsonData.section_id_product;
delete jsonData.section_id_product;
if (Grocy.IsMealPlanEntryEditAction) if (Grocy.IsMealPlanEntryEditAction)
{ {
@ -939,16 +978,19 @@ $(document).on("click", ".mealplan-entry-undone-button", function(e)
$(window).one("resize", function() $(window).one("resize", function()
{ {
// Automatically switch the calendar to "basicDay" view on small screens // Automatically switch the calendar to "agendaDay" view on small screens and to "agendaWeek" otherwise
// and to "basicWeek" otherwise var windowWidth = $(window).width();
if ($(window).width() < 768) $(".calendar").each(function()
{ {
calendar.fullCalendar("changeView", "basicDay"); if (windowWidth < 768)
} {
else $(this).fullCalendar("changeView", "agendaDay");
{ }
calendar.fullCalendar("changeView", "basicWeek"); else
} {
$(this).fullCalendar("changeView", "agendaWeek");
}
});
}); });
Grocy.Components.ProductPicker.GetPicker().on('change', function(e) 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}', '\Grocy\Controllers\RecipesController:RecipeEditForm');
$group->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm'); $group->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm');
$group->get('/mealplan', '\Grocy\Controllers\RecipesController:MealPlan'); $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'); $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, 4);
INSERT INTO recipes_nestings(recipe_id, includes_recipe_id) VALUES (6, 5); 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_sections (name, sort_number) VALUES ('{$this->__t_sql('Breakfast')}', 10);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$tuesdayThisWeek}', 2); INSERT INTO meal_plan_sections (name, sort_number) VALUES ('{$this->__t_sql('Lunch')}', 20);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$wednesdayThisWeek}', 3); INSERT INTO meal_plan_sections (name, sort_number) VALUES ('{$this->__t_sql('Dinner')}', 30);
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, section_id) VALUES ('{$mondayThisWeek}', 1, 2);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$saturdayThisWeek}', 2); INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$tuesdayThisWeek}', 2, 2);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$sundayThisWeek}', 4); INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$wednesdayThisWeek}', 3, 3);
INSERT INTO meal_plan(day, type, note) VALUES ('{$tuesdayThisWeek}', 'note', '{$this->__t_sql('This is a note')}'); INSERT INTO meal_plan(day, recipe_id, section_id) VALUES ('{$thursdayThisWeek}', 4, 1);
INSERT INTO meal_plan(day, type, product_id, product_amount) VALUES (DATE('{$mondayThisWeek}', '-1 days'), 'product', 3, 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) 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 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" data-placement="right"
title="{{ $__t('Meal plan') }}" title="{{ $__t('Meal plan') }}"
data-nav-for-page="mealplan"> 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') }}"> href="{{ $U('/mealplan') }}">
<i class="fas fa-paper-plane"></i> <i class="fas fa-paper-plane"></i>
<span class="nav-link-text">{{ $__t('Meal plan') }}</span> <span class="nav-link-text">{{ $__t('Meal plan') }}</span>

View File

@ -24,6 +24,39 @@
max-height: 140px; 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) { @media (min-width: 400px) {
.table-inline-menu.dropdown-menu { .table-inline-menu.dropdown-menu {
width: 200px !important; width: 200px !important;
@ -47,17 +80,56 @@
<div class="row"> <div class="row">
<div class="col"> <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>
</div> </div>
<hr class="my-2"> <hr class="my-2">
@foreach($usedMealplanSections as $mealplanSection)
<div class="row"> <div class="row">
<div class="col"> <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>
</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" <div class="modal fade"
id="add-recipe-modal" id="add-recipe-modal"
@ -87,6 +159,18 @@
'additionalCssClasses' => 'locale-number-input locale-number-quantity-amount' '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" <input type="hidden"
id="day" id="day"
name="day" name="day"
@ -130,6 +214,18 @@
name="note"></textarea> name="note"></textarea>
</div> </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" <input type="hidden"
name="type" name="type"
value="note"> value="note">
@ -171,6 +267,18 @@
'additionalGroupCssClasses' => 'mb-0' '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" <input type="hidden"
name="type" name="type"
value="product"> 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