Added first basic version of meal planning (references #146)

This commit is contained in:
Bernd Bestel 2019-05-06 19:38:47 +02:00
parent e240260f9f
commit 57233dba1a
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
11 changed files with 232 additions and 2 deletions

View File

@ -3,6 +3,8 @@
- Userfields can have types (Text, Number, Date, etc.)
- Will be shown / can be filled on the edit page of the corresponding entity and will also optionally show in the corresponding tables (inclcudes overview pages)
- => Can be configured under Master data / Userfields
- New feature: Meal planning
- Simple approach for the beginning (more to come): A week view where you can add recipes for each day (new menu entry in the sidebar, below calendar)
- General improvements
- The "expires soon" or "due soon" days (yelllow bar at the top of each overview page) can now be configured
- => New settings page for each area under the settings icon at the top right

View File

@ -115,4 +115,27 @@ class RecipesController extends BaseController
]);
}
}
public function MealPlan(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$recipes = $this->Database->recipes()->fetchAll();
$events = array();
foreach($this->Database->meal_plan() as $mealPlanEntry)
{
$events[] = array(
'id' => $mealPlanEntry['id'],
'title' => FindObjectInArrayByPropertyValue($recipes, 'id', $mealPlanEntry['recipe_id'])->name,
'start' => $mealPlanEntry['day'],
'date_format' => 'date',
'recipe' => json_encode(FindObjectInArrayByPropertyValue($recipes, 'id', $mealPlanEntry['recipe_id'])),
'mealPlanEntry' => json_encode($mealPlanEntry)
);
}
return $this->AppContainer->view->render($response, 'mealplan', [
'fullcalendarEventSources' => $events,
'recipes' => $recipes
]);
}
}

View File

@ -2153,7 +2153,8 @@
"product_groups",
"equipment",
"api_keys",
"userfields"
"userfields",
"meal_plan"
]
},
"ExposedEntitiesPreventListing": {

View File

@ -1230,3 +1230,9 @@ msgstr ""
msgid "Consume %1$s of %2$s"
msgstr ""
msgid "Meal plan"
msgstr ""
msgid "Add recipe to %s"
msgstr ""

8
migrations/0070.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE meal_plan (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
day DATE NOT NULL,
recipe_id INTEGER NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(day, recipe_id)
);

View File

@ -207,8 +207,15 @@ input::-webkit-inner-spin-button {
padding-right: 0.75rem !important;
}
.btn-group-xs > .btn, .btn-xs {
padding : 0.25rem 0.4rem;
font-size : 0.875rem;
line-height : 0.5;
border-radius: 0.2rem;
}
/* Third party component customizations - DataTables */
td {
.dataTable td {
vertical-align: middle !important;
}

100
public/viewjs/mealplan.js Normal file
View File

@ -0,0 +1,100 @@
var calendar = $("#calendar").fullCalendar({
"themeSystem": "bootstrap4",
"header": {
"left": "title",
"center": "",
"right": "prev,today,next"
},
"weekNumbers": false,
"eventLimit": true,
"eventSources": fullcalendarEventSources,
"defaultView": "basicWeek",
"viewRender": function(view)
{
$(".fc-day-header").append('<a class="ml-1 btn btn-outline-dark btn-xs my-1 add-recipe-button" href="#"><i class="fas fa-plus"></i></a>');
},
"eventRender": function(event, element)
{
var recipe = JSON.parse(event.recipe);
element.removeClass("fc-event");
element.addClass("text-center");
element.attr("data-recipe", event.recipe);
element.attr("data-meal-plan-entry", event.mealPlanEntry);
element.html('<h5 class="text-truncate">' + recipe.name + '<br><a class="ml-1 btn btn-outline-danger btn-xs remove-recipe-button" href="#"><i class="fas fa-trash"></i></a></h5>');
if (recipe.picture_file_name && !recipe.picture_file_name.isEmpty())
{
element.html(element.html() + '<img src="' + U("/api/files/recipepictures/") + btoa(recipe.picture_file_name) + '" class="img-fluid">')
}
}
});
$(document).on("click", ".add-recipe-button", function(e)
{
var day = $(this).parent().data("date");
$("#add-recipe-modal-title").text(__t("Add recipe to %s", day.toString()));
$("#day").val(day.toString());
Grocy.Components.RecipePicker.Clear();
$("#add-recipe-modal").modal("show");
Grocy.FrontendHelpers.ValidateForm("add-recipe-form");
});
$("#add-recipe-modal").on("shown.bs.modal", function(e)
{
Grocy.Components.RecipePicker.GetInputElement().focus();
})
$(document).on("click", ".remove-recipe-button", function(e)
{
var mealPlanEntry = JSON.parse($(this).parent().parent().attr("data-meal-plan-entry"));
Grocy.Api.Delete('objects/meal_plan/' + mealPlanEntry.id.toString(), { },
function(result)
{
calendar.fullCalendar('removeEvents', [mealPlanEntry.id]);
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
});
$('#save-add-recipe-button').on('click', function(e)
{
e.preventDefault();
if (document.getElementById("add-recipe-form").checkValidity() === false) //There is at least one validation error
{
return false;
}
Grocy.Api.Post('objects/meal_plan', $('#add-recipe-form').serializeJSON(),
function(result)
{
window.location.reload();
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
});
Grocy.Components.RecipePicker.GetInputElement().keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById("add-recipe-form").checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$("#save-add-recipe-button").click();
}
}
});

View File

@ -57,6 +57,7 @@ $app->group('', function()
$this->get('/recipes', '\Grocy\Controllers\RecipesController:Overview');
$this->get('/recipe/{recipeId}', '\Grocy\Controllers\RecipesController:RecipeEditForm');
$this->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm');
$this->get('/mealplan', '\Grocy\Controllers\RecipesController:MealPlan');
}
// Chore routes

View File

@ -22,6 +22,14 @@ class DemoDataGeneratorService extends BaseService
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
$loremIpsumWithHtmlFormattings = "<h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p><ul><li>At vero eos et accusam et justo duo dolores et ea rebum.</li><li>Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</li></ul><h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur \r\nsadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et \r\ndolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et\r\n justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea \r\ntakimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit \r\namet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>,\r\n sed diam nonumy eirmod tempor invidunt ut labore et dolore magna \r\naliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo \r\ndolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus \r\nest Lorem ipsum dolor sit amet.</p>";
$mondayThisWeek = date('Y-m-d', strtotime('monday this week'));
$tuesdayThisWeek = date('Y-m-d', strtotime('tuesday this week'));
$wednesdayThisWeek = date('Y-m-d', strtotime('wednesday this week'));
$thursdayThisWeek = date('Y-m-d', strtotime('thursday this week'));
$fridayThisWeek = date('Y-m-d', strtotime('friday this week'));
$saturdayThisWeek = date('Y-m-d', strtotime('saturday this week'));
$sundayThisWeek = date('Y-m-d', strtotime('sunday this week'));
$sql = "
UPDATE users SET username = '{$this->__t_sql('Demo User')}' WHERE id = 1;
INSERT INTO users (username, password) VALUES ('{$this->__t_sql('Demo User')} 2', 'x');
@ -103,6 +111,14 @@ 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 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('Cleaned the kitchen floor')}', 'dynamic-regular', 7); --2
INSERT INTO chores (name, period_type, period_days) VALUES ('{$this->__t_sql('Lawn mowed in the garden')}', 'dynamic-regular', 21); --3

View File

@ -175,6 +175,12 @@
<span class="nav-link-text">{{ $__t('Calendar') }}</span>
</a>
</li>
<li class="nav-item" data-toggle="tooltip" data-placement="right" title="{{ $__t('Meal plan') }}" data-nav-for-page="mealplan">
<a class="nav-link discrete-link" href="{{ $U('/mealplan') }}">
<i class="fas fa-paper-plane"></i>
<span class="nav-link-text">{{ $__t('Meal plan') }}</span>
</a>
</li>
@endif
<li class="nav-item mt-4" data-toggle="tooltip" data-placement="right" title="{{ $__t('Manage master data') }}">

60
views/mealplan.blade.php Normal file
View File

@ -0,0 +1,60 @@
@extends('layout.default')
@section('title', $__t('Meal plan'))
@section('activeNav', 'mealplan')
@section('viewJsName', 'mealplan')
@push('pageScripts')
<script src="{{ $U('/node_modules/fullcalendar/dist/fullcalendar.min.js?v=', true) }}{{ $version }}"></script>
@if(!empty($__t('fullcalendar_locale') && $__t('fullcalendar_locale') != 'x'))<script src="{{ $U('/node_modules', true) }}/fullcalendar/dist/locale/{{ $__t('fullcalendar_locale') }}.js?v={{ $version }}"></script>@endif
@endpush
@push('pageStyles')
<link href="{{ $U('/node_modules/fullcalendar/dist/fullcalendar.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@section('content')
<script>
var fullcalendarEventSources = {!! json_encode(array($fullcalendarEventSources)) !!}
</script>
<div class="row">
<div class="col">
<h1>
@yield('title')
</h1>
</div>
</div>
<div class="row">
<div class="col">
<div id="calendar"></div>
</div>
</div>
<div class="modal fade" id="add-recipe-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content text-center">
<div class="modal-header">
<h4 id="add-recipe-modal-title" class="modal-title w-100"></h4>
</div>
<div class="modal-body">
<form id="add-recipe-form" novalidate>
@include('components.recipepicker', array(
'recipes' => $recipes,
'isRequired' => true
))
<input type="hidden" id="day" name="day" value="">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Cancel') }}</button>
<button id="save-add-recipe-button" data-dismiss="modal" class="btn btn-success">{{ $__t('Save') }}</button>
</div>
</div>
</div>
</div>
@stop