New Feature Recipe Catagories

This commit is contained in:
Kurt Riddlesperger 2020-02-09 19:56:42 -06:00
parent 49edd011b4
commit b3aa497fb7
13 changed files with 385 additions and 1 deletions

View File

@ -63,6 +63,7 @@ class RecipesController extends BaseController
'recipes' => $recipes, 'recipes' => $recipes,
'recipesResolved' => $recipesResolved, 'recipesResolved' => $recipesResolved,
'recipePositionsResolved' => $this->Database->recipes_pos_resolved(), 'recipePositionsResolved' => $this->Database->recipes_pos_resolved(),
'recipeCatagories' => $this->Database->recipe_catagories()->orderBy('name'),
'selectedRecipe' => $selectedRecipe, 'selectedRecipe' => $selectedRecipe,
'selectedRecipePositionsResolved' => $selectedRecipePositionsResolved, 'selectedRecipePositionsResolved' => $selectedRecipePositionsResolved,
'products' => $this->Database->products(), 'products' => $this->Database->products(),
@ -94,6 +95,7 @@ class RecipesController extends BaseController
return $this->AppContainer->view->render($response, 'recipeform', [ return $this->AppContainer->view->render($response, 'recipeform', [
'recipe' => $this->Database->recipes($recipeId), 'recipe' => $this->Database->recipes($recipeId),
'recipePositions' => $this->Database->recipes_pos()->where('recipe_id', $recipeId), 'recipePositions' => $this->Database->recipes_pos()->where('recipe_id', $recipeId),
'recipeCatagories' => $this->Database->recipe_catagories()->orderBy('name'),
'mode' => 'edit', 'mode' => 'edit',
'products' => $this->Database->products()->orderBy('name'), 'products' => $this->Database->products()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units(), 'quantityunits' => $this->Database->quantity_units(),
@ -132,6 +134,30 @@ class RecipesController extends BaseController
} }
} }
public function RecipeCatagoriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'recipecatagories', [
'recipeCatagories' => $this->Database->recipe_catagories()->orderBy('name')
]);
}
public function RecipeCatagoryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['recipeCatagoryId'] == 'new')
{
return $this->AppContainer->view->render($response, 'recipecatagoryform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'recipecatagoryform', [
'recipeCatagory' => $this->Database->recipe_catagories($args['recipeCatagoryId']),
'mode' => 'edit'
]);
}
}
public function MealPlan(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function MealPlan(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
$recipes = $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll(); $recipes = $this->Database->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->fetchAll();

View File

@ -3304,6 +3304,7 @@
"shopping_list", "shopping_list",
"shopping_lists", "shopping_lists",
"recipes", "recipes",
"recipe_catagories",
"recipes_pos", "recipes_pos",
"recipes_nestings", "recipes_nestings",
"tasks", "tasks",
@ -3329,6 +3330,7 @@
"shopping_list", "shopping_list",
"shopping_lists", "shopping_lists",
"recipes", "recipes",
"recipe_catagories",
"recipes_pos", "recipes_pos",
"recipes_nestings", "recipes_nestings",
"tasks", "tasks",

9
migrations/0098.sql Normal file
View File

@ -0,0 +1,9 @@
ALTER TABLE recipes
ADD recipe_catagory_id INTEGER;
CREATE TABLE recipe_catagories (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

View File

@ -0,0 +1,68 @@
Grocy.Components.RecipeCatagoryPicker = { };
Grocy.Components.RecipeCatagoryPicker.GetPicker = function()
{
return $('#recipe_catagory_id');
}
Grocy.Components.RecipeCatagoryPicker.GetInputElement = function()
{
return $('#recipe_catagory_id_text_input');
}
Grocy.Components.RecipeCatagoryPicker.GetValue = function()
{
return $('#recipe_catagory_id').val();
}
Grocy.Components.RecipeCatagoryPicker.SetValue = function(value)
{
Grocy.Components.RecipeCatagoryPicker.GetInputElement().val(value);
Grocy.Components.RecipeCatagoryPicker.GetInputElement().trigger('change');
}
Grocy.Components.RecipeCatagoryPicker.SetId = function(value)
{
Grocy.Components.RecipeCatagoryPicker.GetPicker().val(value);
Grocy.Components.RecipeCatagoryPicker.GetPicker().data('combobox').refresh();
Grocy.Components.RecipeCatagoryPicker.GetInputElement().trigger('change');
}
Grocy.Components.RecipeCatagoryPicker.Clear = function()
{
Grocy.Components.RecipeCatagoryPicker.SetValue('');
Grocy.Components.RecipeCatagoryPicker.SetId(null);
}
$('.recipe-catagory-combobox').combobox({
appendId: '_text_input',
bsVersion: '4',
clearIfNoMatch: true
});
var prefillByName = Grocy.Components.RecipeCatagoryPicker.GetPicker().parent().data('prefill-by-name').toString();
if (typeof prefillByName !== "undefined")
{
possibleOptionElement = $("#recipe_catagory_id option:contains(\"" + prefillByName + "\")").first();
if (possibleOptionElement.length > 0)
{
$('#recipe_catagory_id').val(possibleOptionElement.val());
$('#recipe_catagory_id').data('combobox').refresh();
$('#recipe_catagory_id').trigger('change');
var nextInputElement = $(Grocy.Components.RecipeCatagoryPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus();
}
}
var prefillById = Grocy.Components.RecipeCatagoryPicker.GetPicker().parent().data('prefill-by-id').toString();
if (typeof prefillById !== "undefined")
{
$('#recipe_catagory_id').val(prefillById);
$('#recipe_catagory_id').data('combobox').refresh();
$('#recipe_catagory_id').trigger('change');
var nextInputElement = $(Grocy.Components.RecipeCatagoryPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus();
}

View File

@ -0,0 +1,57 @@
var recipecatagoriesTable = $('#recipecatagories-table').DataTable({
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
]
});
$('#recipecatagories-table tbody').removeClass("d-none");
recipecatagoriesTable.columns.adjust().draw();
$("#search").on("keyup", Delay(function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
recipecatagoriesTable.search(value).draw();
}, 200));
$(document).on('click', '.recipe-catagory-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-recipecatagory-name');
var objectId = $(e.currentTarget).attr('data-recipecatagory-id');
bootbox.confirm({
message: __t('Are you sure to delete recipe catagory "%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/recipe_catagories/' + objectId, {},
function(result)
{
window.recipecatagory.href = U('/recipecatagories');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});

View File

@ -0,0 +1,63 @@
$('#save-recipe-catagory-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#recipe-catagory-form').serializeJSON();
Grocy.FrontendHelpers.BeginUiBusy("recipe-catagory-form");
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('objects/recipe_catagories', jsonData,
function(result)
{
Grocy.EditObjectId = result.created_object_id;
window.location.href = U('/recipecatagories');
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("recipe-catagory-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
Grocy.Api.Put('objects/recipe_catagories/' + Grocy.EditObjectId, jsonData,
function(result)
{
window.location.href = U('/recipecatagories');
},
function(xhr)
{
Grocy.FrontendHelpers.EndUiBusy("recipe-catagory-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#recipe-catagory-form input').keyup(function (event)
{
Grocy.FrontendHelpers.ValidateForm('recipe-catagory-form');
});
$('#recipe-catagory-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('recipe-catagory-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-recipe-catagory-button').click();
}
}
});
Grocy.Components.UserfieldsForm.Load();
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('recipe-catagory-form');

View File

@ -70,6 +70,8 @@ $app->group('', function()
$this->get('/recipe/{recipeId}', '\Grocy\Controllers\RecipesController:RecipeEditForm'); $this->get('/recipe/{recipeId}', '\Grocy\Controllers\RecipesController:RecipeEditForm');
$this->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm'); $this->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm');
$this->get('/mealplan', '\Grocy\Controllers\RecipesController:MealPlan'); $this->get('/mealplan', '\Grocy\Controllers\RecipesController:MealPlan');
$this->get('/recipecatagories', '\Grocy\Controllers\RecipesController:RecipeCatagoriesList');
$this->get('/recipecatagory/{recipeCatagoryId}', '\Grocy\Controllers\RecipesController:RecipeCatagoryEditForm');
} }
// Chore routes // Chore routes

View File

@ -0,0 +1,21 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/recipecatagorypicker.js', true) }}?v={{ $version }}"></script>
@endpush
@php if(empty($prefillByName)) { $prefillByName = ''; } @endphp
@php if(empty($prefillById)) { $prefillById = ''; } @endphp
@php if(!isset($isRequired)) { $isRequired = true; } @endphp
@php if(empty($hint)) { $hint = ''; } @endphp
@php if(empty($hintId)) { $hintId = ''; } @endphp
@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-name="{{ $prefillByName }}" data-prefill-by-id="{{ $prefillById }}">
<label for="recipe_catagory_id">{{ $__t('Recipe Catagory') }}&nbsp;&nbsp;<span id="{{ $hintId }}" class="small text-muted">{{ $hint }}</span></label>
<select class="form-control recipe-catagory-combobox" id="recipe_catagory_id" name="recipe_catagory_id" @if($isRequired) required @endif>
<option value=""></option>
@foreach($recipeCatagories as $recipeCatagory)
<option value="{{ $recipeCatagory->id }}">{{ $recipeCatagory->name }}</option>
@endforeach
</select>
<div class="invalid-feedback">{{ $__t('You have to select a recipe catagory') }}</div>
</div>

View File

@ -235,6 +235,14 @@
<span class="nav-link-text">{{ $__t('Products') }}</span> <span class="nav-link-text">{{ $__t('Products') }}</span>
</a> </a>
</li> </li>
@if(GROCY_FEATURE_FLAG_RECIPES)
<li data-nav-for-page="recipecatagories" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/recipecatagories') }}">
<i class="fas fa-map-marker-alt"></i>
<span class="nav-link-text">{{ $__t('Recipe catagories') }}</span>
</a>
</li>
@endif
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) @if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
<li data-nav-for-page="locations" data-sub-menu-of="#top-nav-manager-master-data"> <li data-nav-for-page="locations" data-sub-menu-of="#top-nav-manager-master-data">
<a class="nav-link discrete-link" href="{{ $U('/locations') }}"> <a class="nav-link discrete-link" href="{{ $U('/locations') }}">

View File

@ -0,0 +1,73 @@
@extends('layout.default')
@section('title', $__t('Recipe Catagories'))
@section('activeNav', 'recipecatagories')
@section('viewJsName', 'recipecatagories')
@section('content')
<div class="row">
<div class="col">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/recipecatagory/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $__t('Add') }}
</a>
<a class="btn btn-outline-secondary" href="{{ $U('/userfields?entity=recipe_catagories') }}">
<i class="fas fa-sliders-h"></i>&nbsp;{{ $__t('Configure userfields') }}
</a>
</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $__t('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="recipecatagories-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th class="border-right"></th>
<th>{{ $__t('Name') }}</th>
<th>{{ $__t('Description') }}</th>
@include('components.userfields_thead', array(
'userfields' => $userfields
))
</tr>
</thead>
<tbody class="d-none">
@foreach($recipecatagories as $recipecatagory)
<tr>
<td class="fit-content border-right">
<a class="btn btn-info btn-sm" href="{{ $U('/recipecatagory/') }}{{ $recipecatagory->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm recipe-catagory-delete-button" href="#" data-recipecatagory-id="{{ $recipecatagory->id }}" data-recipecatagory-name="{{ $recipecatagory->name }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $recipecatagory->name }}
</td>
<td>
{{ $recipecatagory->description }}
</td>
@include('components.userfields_tbody', array(
'userfields' => $userfields,
'userfieldValues' => FindAllObjectsInArrayByPropertyValue($userfieldValues, 'object_id', $recipecatagory->id)
))
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@ -0,0 +1,41 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $__t('Edit recipe catagory'))
@else
@section('title', $__t('Create recipe catagory'))
@endif
@section('viewJsName', 'recipecatagoryform')
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $recipecatagory->id }};</script>
@endif
<form id="recipe-catagory-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'){{ $recipecatagory->name }}@endif">
<div class="invalid-feedback">{{ $__t('A name is required') }}</div>
</div>
<div class="form-group">
<label for="description">{{ $__t('Description') }}</label>
<textarea class="form-control" rows="2" id="description" name="description">@if($mode == 'edit'){{ $recipecatagory->description }}@endif</textarea>
</div>
<button id="save-recipe-catagory-button" class="btn btn-success">{{ $__t('Save') }}</button>
</form>
</div>
</div>
@stop

View File

@ -61,7 +61,11 @@
'value' => $value, 'value' => $value,
'invalidFeedback' => $__t('This cannot be lower than %s', '1'), 'invalidFeedback' => $__t('This cannot be lower than %s', '1'),
'hint' => $__t('The ingredients listed here result in this amount of servings') 'hint' => $__t('The ingredients listed here result in this amount of servings')
)) ))
@include('components.recipecatagorypicker', array(
'recipecatagories' => $recipecatagory
))
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">

View File

@ -35,6 +35,12 @@
<option class="bg-danger" value="notenoughinstock">{{ $__t('Not enough in stock') }}</option> <option class="bg-danger" value="notenoughinstock">{{ $__t('Not enough in stock') }}</option>
</select> </select>
</div> </div>
<div class="col-6">
@include('components.recipecatagorypicker', array(
'recipeCatagories' => $recipeCatagory
))
</div>
</div> </div>
<ul class="nav nav-tabs mt-3"> <ul class="nav nav-tabs mt-3">
@ -58,6 +64,7 @@
<th class="d-none">Hidden status for sorting of "Requirements fulfilled" column</th> <th class="d-none">Hidden status for sorting of "Requirements fulfilled" column</th>
<th class="d-none">Hidden status for filtering by status</th> <th class="d-none">Hidden status for filtering by status</th>
<th class="d-none">Hidden recipe ingredient product names</th> <th class="d-none">Hidden recipe ingredient product names</th>
<th class="d-none">Hidden recipe catagory names</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', array(
'userfields' => $userfields 'userfields' => $userfields
@ -89,6 +96,9 @@
{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name . ' ' }} {{ FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name . ' ' }}
@endforeach @endforeach
</td> </td>
<td>
{{ FindObjectInArrayByPropertyValue($recipeCatagories, 'id', $recipe->recipy_catagory_id)->name }}
</td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', array(
'userfields' => $userfields, 'userfields' => $userfields,