mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Added first basic version of meal planning (references #146)
This commit is contained in:
parent
e240260f9f
commit
57233dba1a
@ -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
|
||||
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -2153,7 +2153,8 @@
|
||||
"product_groups",
|
||||
"equipment",
|
||||
"api_keys",
|
||||
"userfields"
|
||||
"userfields",
|
||||
"meal_plan"
|
||||
]
|
||||
},
|
||||
"ExposedEntitiesPreventListing": {
|
||||
|
@ -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
8
migrations/0070.sql
Normal 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)
|
||||
);
|
@ -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
100
public/viewjs/mealplan.js
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
60
views/mealplan.blade.php
Normal 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
|
Loading…
x
Reference in New Issue
Block a user