Merge pull request #1447 from smilerz/mealplan_shopping_useability
Mealplan shopping useability
This commit is contained in:
commit
b2a415b333
@ -205,9 +205,9 @@ class CustomIsShared(permissions.BasePermission):
|
|||||||
return request.user.is_authenticated
|
return request.user.is_authenticated
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj):
|
def has_object_permission(self, request, view, obj):
|
||||||
# temporary hack to make old shopping list work with new shopping list
|
# # temporary hack to make old shopping list work with new shopping list
|
||||||
if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
|
# if obj.__class__.__name__ in ['ShoppingList', 'ShoppingListEntry']:
|
||||||
return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
|
# return is_object_shared(request.user, obj) or obj.created_by in list(request.user.get_shopping_share())
|
||||||
return is_object_shared(request.user, obj)
|
return is_object_shared(request.user, obj)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from django.utils import timezone
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from cookbook.helper.HelperFunctions import Round, str2bool
|
from cookbook.helper.HelperFunctions import Round, str2bool
|
||||||
from cookbook.models import (Ingredient, ShoppingListEntry, ShoppingListRecipe,
|
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
|
||||||
SupermarketCategoryRelation)
|
SupermarketCategoryRelation)
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
|
||||||
@ -38,118 +38,272 @@ def shopping_helper(qs, request):
|
|||||||
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
|
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
|
||||||
|
|
||||||
|
|
||||||
# TODO refactor as class
|
class RecipeShoppingEditor():
|
||||||
def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
|
def __init__(self, user, space, **kwargs):
|
||||||
"""
|
self.created_by = user
|
||||||
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
|
self.space = space
|
||||||
:param list_recipe: Modify an existing ShoppingListRecipe
|
self._kwargs = {**kwargs}
|
||||||
:param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
|
|
||||||
:param mealplan: alternatively use a mealplan recipe as source of ingredients
|
|
||||||
:param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
|
|
||||||
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
|
||||||
:param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
|
||||||
"""
|
|
||||||
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
|
||||||
if not r:
|
|
||||||
raise ValueError(_("You must supply a recipe or mealplan"))
|
|
||||||
|
|
||||||
created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
|
self.mealplan = self._kwargs.get('mealplan', None)
|
||||||
if not created_by:
|
if type(self.mealplan) in [int, float]:
|
||||||
raise ValueError(_("You must supply a created_by"))
|
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
|
||||||
|
self.id = self._kwargs.get('id', None)
|
||||||
|
|
||||||
try:
|
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
|
||||||
servings = float(servings)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
servings = getattr(mealplan, 'servings', 1.0)
|
|
||||||
|
|
||||||
servings_factor = servings / r.servings
|
if self._shopping_list_recipe:
|
||||||
|
# created_by needs to be sticky to original creator as it is 'their' shopping list
|
||||||
|
# changing shopping list created_by can shift some items to new owner which may not share in the other direction
|
||||||
|
self.created_by = getattr(self._shopping_list_recipe.entries.first(), 'created_by', self.created_by)
|
||||||
|
|
||||||
shared_users = list(created_by.get_shopping_share())
|
self.recipe = getattr(self._shopping_list_recipe, 'recipe', None) or self._kwargs.get('recipe', None) or getattr(self.mealplan, 'recipe', None)
|
||||||
shared_users.append(created_by)
|
if type(self.recipe) in [int, float]:
|
||||||
if list_recipe:
|
self.recipe = Recipe.objects.filter(id=self.recipe, space=self.space)
|
||||||
created = False
|
|
||||||
else:
|
|
||||||
list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
|
|
||||||
created = True
|
|
||||||
|
|
||||||
related_step_ing = []
|
try:
|
||||||
if servings == 0 and not created:
|
self.servings = float(self._kwargs.get('servings', None))
|
||||||
list_recipe.delete()
|
except (ValueError, TypeError):
|
||||||
return []
|
self.servings = getattr(self._shopping_list_recipe, 'servings', None) or getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', None)
|
||||||
elif ingredients:
|
|
||||||
ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
|
|
||||||
else:
|
|
||||||
ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
|
|
||||||
|
|
||||||
if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
@property
|
||||||
ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
def _servings_factor(self):
|
||||||
|
return self.servings / self.recipe.servings
|
||||||
|
|
||||||
if related := created_by.userpreference.mealplan_autoinclude_related:
|
@property
|
||||||
# TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
|
def _shared_users(self):
|
||||||
related_recipes = r.get_related_recipes()
|
return [*list(self.created_by.get_shopping_share()), self.created_by]
|
||||||
|
|
||||||
for x in related_recipes:
|
@staticmethod
|
||||||
# related recipe is a Step serving size is driven by recipe serving size
|
def get_shopping_list_recipe(id, user, space):
|
||||||
# TODO once/if Steps can have a serving size this needs to be refactored
|
return ShoppingListRecipe.objects.filter(id=id).filter(Q(shoppinglist__space=space) | Q(entries__space=space)).filter(
|
||||||
if exclude_onhand:
|
Q(shoppinglist__created_by=user)
|
||||||
# if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
|
| Q(shoppinglist__shared=user)
|
||||||
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
|
| Q(entries__created_by=user)
|
||||||
else:
|
| Q(entries__created_by__in=list(user.get_shopping_share()))
|
||||||
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
|
).prefetch_related('entries').first()
|
||||||
|
|
||||||
x_ing = []
|
def get_recipe_ingredients(self, id, exclude_onhand=False):
|
||||||
if ingredients.filter(food__recipe=x).exists():
|
if exclude_onhand:
|
||||||
for ing in ingredients.filter(food__recipe=x):
|
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space).exclude(food__onhand_users__id__in=[x.id for x in self._shared_users])
|
||||||
if exclude_onhand:
|
else:
|
||||||
x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
|
||||||
else:
|
|
||||||
x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
|
|
||||||
for i in [x for x in x_ing]:
|
|
||||||
ShoppingListEntry.objects.create(
|
|
||||||
list_recipe=list_recipe,
|
|
||||||
food=i.food,
|
|
||||||
unit=i.unit,
|
|
||||||
ingredient=i,
|
|
||||||
amount=i.amount * Decimal(servings_factor),
|
|
||||||
created_by=created_by,
|
|
||||||
space=space,
|
|
||||||
)
|
|
||||||
# dont' add food to the shopping list that are actually recipes that will be added as ingredients
|
|
||||||
ingredients = ingredients.exclude(food__recipe=x)
|
|
||||||
|
|
||||||
add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
|
@property
|
||||||
if not append:
|
def _include_related(self):
|
||||||
existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
|
return self.created_by.userpreference.mealplan_autoinclude_related
|
||||||
# delete shopping list entries not included in ingredients
|
|
||||||
existing_list.exclude(ingredient__in=ingredients).delete()
|
|
||||||
# add shopping list entries that did not previously exist
|
|
||||||
add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
|
||||||
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
|
||||||
|
|
||||||
# if servings have changed, update the ShoppingListRecipe and existing Entries
|
@property
|
||||||
if servings <= 0:
|
def _exclude_onhand(self):
|
||||||
servings = 1
|
return self.created_by.userpreference.mealplan_autoexclude_onhand
|
||||||
|
|
||||||
if not created and list_recipe.servings != servings:
|
def create(self, **kwargs):
|
||||||
update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
|
ingredients = kwargs.get('ingredients', None)
|
||||||
list_recipe.servings = servings
|
exclude_onhand = not ingredients and self._exclude_onhand
|
||||||
list_recipe.save()
|
if servings := kwargs.get('servings', None):
|
||||||
for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
|
self.servings = float(servings)
|
||||||
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
|
||||||
|
if mealplan := kwargs.get('mealplan', None):
|
||||||
|
self.mealplan = mealplan
|
||||||
|
self.recipe = mealplan.recipe
|
||||||
|
elif recipe := kwargs.get('recipe', None):
|
||||||
|
self.recipe = recipe
|
||||||
|
|
||||||
|
if not self.servings:
|
||||||
|
self.servings = getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', 1.0)
|
||||||
|
|
||||||
|
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings)
|
||||||
|
|
||||||
|
if ingredients:
|
||||||
|
self._add_ingredients(ingredients=ingredients)
|
||||||
|
else:
|
||||||
|
if self._include_related:
|
||||||
|
related = self.recipe.get_related_recipes()
|
||||||
|
self._add_ingredients(self.get_recipe_ingredients(self.recipe.id, exclude_onhand=exclude_onhand).exclude(food__recipe__in=related))
|
||||||
|
for r in related:
|
||||||
|
self._add_ingredients(self.get_recipe_ingredients(r.id, exclude_onhand=exclude_onhand).exclude(food__recipe__in=related))
|
||||||
|
else:
|
||||||
|
self._add_ingredients(self.get_recipe_ingredients(self.recipe.id, exclude_onhand=exclude_onhand))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add(self, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
def edit(self, servings=None, ingredients=None, **kwargs):
|
||||||
|
if servings:
|
||||||
|
self.servings = servings
|
||||||
|
|
||||||
|
self._delete_ingredients(ingredients=ingredients)
|
||||||
|
if self.servings != self._shopping_list_recipe.servings:
|
||||||
|
self.edit_servings()
|
||||||
|
self._add_ingredients(ingredients=ingredients)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def edit_servings(self, servings=None, **kwargs):
|
||||||
|
if servings:
|
||||||
|
self.servings = servings
|
||||||
|
if id := kwargs.get('id', None):
|
||||||
|
self._shopping_list_recipe = self.get_shopping_list_recipe(id, self.created_by, self.space)
|
||||||
|
if not self.servings:
|
||||||
|
raise ValueError(_("You must supply a servings size"))
|
||||||
|
|
||||||
|
if self._shopping_list_recipe.servings == self.servings:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for sle in ShoppingListEntry.objects.filter(list_recipe=self._shopping_list_recipe):
|
||||||
|
sle.amount = sle.ingredient.amount * Decimal(self._servings_factor)
|
||||||
sle.save()
|
sle.save()
|
||||||
|
self._shopping_list_recipe.servings = self.servings
|
||||||
|
self._shopping_list_recipe.save()
|
||||||
|
return True
|
||||||
|
|
||||||
# add any missing Entries
|
def delete(self, **kwargs):
|
||||||
for i in [x for x in add_ingredients if x.food]:
|
try:
|
||||||
|
self._shopping_list_recipe.delete()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
ShoppingListEntry.objects.create(
|
def _add_ingredients(self, ingredients=None):
|
||||||
list_recipe=list_recipe,
|
if not ingredients:
|
||||||
food=i.food,
|
return
|
||||||
unit=i.unit,
|
elif type(ingredients) == list:
|
||||||
ingredient=i,
|
ingredients = Ingredient.objects.filter(id__in=ingredients)
|
||||||
amount=i.amount * Decimal(servings_factor),
|
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
|
||||||
created_by=created_by,
|
add_ingredients = ingredients.exclude(id__in=existing)
|
||||||
space=space,
|
|
||||||
)
|
|
||||||
|
|
||||||
# return all shopping list items
|
for i in [x for x in add_ingredients if x.food]:
|
||||||
return list_recipe
|
ShoppingListEntry.objects.create(
|
||||||
|
list_recipe=self._shopping_list_recipe,
|
||||||
|
food=i.food,
|
||||||
|
unit=i.unit,
|
||||||
|
ingredient=i,
|
||||||
|
amount=i.amount * Decimal(self._servings_factor),
|
||||||
|
created_by=self.created_by,
|
||||||
|
space=self.space,
|
||||||
|
)
|
||||||
|
|
||||||
|
# deletes shopping list entries not in ingredients list
|
||||||
|
def _delete_ingredients(self, ingredients=None):
|
||||||
|
if not ingredients:
|
||||||
|
return
|
||||||
|
to_delete = self._shopping_list_recipe.entries.exclude(ingredient__in=ingredients)
|
||||||
|
ShoppingListEntry.objects.filter(id__in=to_delete).delete()
|
||||||
|
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
|
||||||
|
|
||||||
|
|
||||||
|
# # TODO refactor as class
|
||||||
|
# def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
|
||||||
|
# """
|
||||||
|
# Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
|
||||||
|
# :param list_recipe: Modify an existing ShoppingListRecipe
|
||||||
|
# :param recipe: Recipe to use as list of ingredients. One of [recipe, mealplan] are required
|
||||||
|
# :param mealplan: alternatively use a mealplan recipe as source of ingredients
|
||||||
|
# :param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
|
||||||
|
# :param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
||||||
|
# :param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
||||||
|
# """
|
||||||
|
# r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
||||||
|
# if not r:
|
||||||
|
# raise ValueError(_("You must supply a recipe or mealplan"))
|
||||||
|
|
||||||
|
# created_by = created_by or getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', None)
|
||||||
|
# if not created_by:
|
||||||
|
# raise ValueError(_("You must supply a created_by"))
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# servings = float(servings)
|
||||||
|
# except (ValueError, TypeError):
|
||||||
|
# servings = getattr(mealplan, 'servings', 1.0)
|
||||||
|
|
||||||
|
# servings_factor = servings / r.servings
|
||||||
|
|
||||||
|
# shared_users = list(created_by.get_shopping_share())
|
||||||
|
# shared_users.append(created_by)
|
||||||
|
# if list_recipe:
|
||||||
|
# created = False
|
||||||
|
# else:
|
||||||
|
# list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
|
||||||
|
# created = True
|
||||||
|
|
||||||
|
# related_step_ing = []
|
||||||
|
# if servings == 0 and not created:
|
||||||
|
# list_recipe.delete()
|
||||||
|
# return []
|
||||||
|
# elif ingredients:
|
||||||
|
# ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
|
||||||
|
# else:
|
||||||
|
# ingredients = Ingredient.objects.filter(step__recipe=r, food__ignore_shopping=False, space=space)
|
||||||
|
|
||||||
|
# if exclude_onhand := created_by.userpreference.mealplan_autoexclude_onhand:
|
||||||
|
# ingredients = ingredients.exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||||
|
|
||||||
|
# if related := created_by.userpreference.mealplan_autoinclude_related:
|
||||||
|
# # TODO: add levels of related recipes (related recipes of related recipes) to use when auto-adding mealplans
|
||||||
|
# related_recipes = r.get_related_recipes()
|
||||||
|
|
||||||
|
# for x in related_recipes:
|
||||||
|
# # related recipe is a Step serving size is driven by recipe serving size
|
||||||
|
# # TODO once/if Steps can have a serving size this needs to be refactored
|
||||||
|
# if exclude_onhand:
|
||||||
|
# # if steps are used more than once in a recipe or subrecipe - I don' think this results in the desired behavior
|
||||||
|
# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users]).values_list('id', flat=True)
|
||||||
|
# else:
|
||||||
|
# related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
|
||||||
|
|
||||||
|
# x_ing = []
|
||||||
|
# if ingredients.filter(food__recipe=x).exists():
|
||||||
|
# for ing in ingredients.filter(food__recipe=x):
|
||||||
|
# if exclude_onhand:
|
||||||
|
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__onhand_users__id__in=[x.id for x in shared_users])
|
||||||
|
# else:
|
||||||
|
# x_ing = Ingredient.objects.filter(step__recipe=x, food__ignore_shopping=False, space=space).exclude(food__ignore_shopping=True)
|
||||||
|
# for i in [x for x in x_ing]:
|
||||||
|
# ShoppingListEntry.objects.create(
|
||||||
|
# list_recipe=list_recipe,
|
||||||
|
# food=i.food,
|
||||||
|
# unit=i.unit,
|
||||||
|
# ingredient=i,
|
||||||
|
# amount=i.amount * Decimal(servings_factor),
|
||||||
|
# created_by=created_by,
|
||||||
|
# space=space,
|
||||||
|
# )
|
||||||
|
# # dont' add food to the shopping list that are actually recipes that will be added as ingredients
|
||||||
|
# ingredients = ingredients.exclude(food__recipe=x)
|
||||||
|
|
||||||
|
# add_ingredients = list(ingredients.values_list('id', flat=True)) + related_step_ing
|
||||||
|
# if not append:
|
||||||
|
# existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
|
||||||
|
# # delete shopping list entries not included in ingredients
|
||||||
|
# existing_list.exclude(ingredient__in=ingredients).delete()
|
||||||
|
# # add shopping list entries that did not previously exist
|
||||||
|
# add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
||||||
|
# add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
||||||
|
|
||||||
|
# # if servings have changed, update the ShoppingListRecipe and existing Entries
|
||||||
|
# if servings <= 0:
|
||||||
|
# servings = 1
|
||||||
|
|
||||||
|
# if not created and list_recipe.servings != servings:
|
||||||
|
# update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
|
||||||
|
# list_recipe.servings = servings
|
||||||
|
# list_recipe.save()
|
||||||
|
# for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
|
||||||
|
# sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
||||||
|
# sle.save()
|
||||||
|
|
||||||
|
# # add any missing Entries
|
||||||
|
# for i in [x for x in add_ingredients if x.food]:
|
||||||
|
|
||||||
|
# ShoppingListEntry.objects.create(
|
||||||
|
# list_recipe=list_recipe,
|
||||||
|
# food=i.food,
|
||||||
|
# unit=i.unit,
|
||||||
|
# ingredient=i,
|
||||||
|
# amount=i.amount * Decimal(servings_factor),
|
||||||
|
# created_by=created_by,
|
||||||
|
# space=space,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # return all shopping list items
|
||||||
|
# return list_recipe
|
||||||
|
20
cookbook/migrations/0168_add_unit_searchfields.py
Normal file
20
cookbook/migrations/0168_add_unit_searchfields.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from cookbook.models import SearchFields
|
||||||
|
|
||||||
|
|
||||||
|
def create_searchfields(apps, schema_editor):
|
||||||
|
SearchFields.objects.create(name='Units', field='steps__ingredients__unit__name')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0167_userpreference_left_handed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
create_searchfields
|
||||||
|
),
|
||||||
|
]
|
@ -609,7 +609,7 @@ class NutritionInformation(models.Model, PermissionModelMixin):
|
|||||||
)
|
)
|
||||||
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
proteins = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||||
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
calories = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||||
source = models.CharField( max_length=512, default="", null=True, blank=True)
|
source = models.CharField(max_length=512, default="", null=True, blank=True)
|
||||||
|
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
objects = ScopedManager(space='space')
|
objects = ScopedManager(space='space')
|
||||||
@ -852,11 +852,12 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Shopping list entry {self.id}'
|
return f'Shopping list entry {self.id}'
|
||||||
|
|
||||||
# TODO deprecate
|
|
||||||
def get_shared(self):
|
def get_shared(self):
|
||||||
return self.shoppinglist_set.first().shared.all()
|
try:
|
||||||
|
return self.shoppinglist_set.first().shared.all()
|
||||||
|
except AttributeError:
|
||||||
|
return self.created_by.userpreference.shopping_share.all()
|
||||||
|
|
||||||
# TODO deprecate
|
|
||||||
def get_owner(self):
|
def get_owner(self):
|
||||||
try:
|
try:
|
||||||
return self.created_by or self.shoppinglist_set.first().created_by
|
return self.created_by or self.shoppinglist_set.first().created_by
|
||||||
@ -881,6 +882,12 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Shopping list {self.id}'
|
return f'Shopping list {self.id}'
|
||||||
|
|
||||||
|
def get_shared(self):
|
||||||
|
try:
|
||||||
|
return self.shared.all() or self.created_by.userpreference.shopping_share.all()
|
||||||
|
except AttributeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
|
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
|
@ -13,7 +13,7 @@ from rest_framework.exceptions import NotFound, ValidationError
|
|||||||
from rest_framework.fields import empty
|
from rest_framework.fields import empty
|
||||||
|
|
||||||
from cookbook.helper.HelperFunctions import str2bool
|
from cookbook.helper.HelperFunctions import str2bool
|
||||||
from cookbook.helper.shopping_helper import list_from_recipe
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||||
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
|
from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food,
|
||||||
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
|
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
|
||||||
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
|
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
|
||||||
@ -660,7 +660,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
|||||||
validated_data['created_by'] = self.context['request'].user
|
validated_data['created_by'] = self.context['request'].user
|
||||||
mealplan = super().create(validated_data)
|
mealplan = super().create(validated_data)
|
||||||
if self.context['request'].data.get('addshopping', False):
|
if self.context['request'].data.get('addshopping', False):
|
||||||
list_from_recipe(mealplan=mealplan, servings=validated_data['servings'], created_by=validated_data['created_by'], space=validated_data['space'])
|
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
|
||||||
|
SLR.create(mealplan=mealplan, servings=validated_data['servings'])
|
||||||
return mealplan
|
return mealplan
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -694,12 +695,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
|||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
# TODO remove once old shopping list
|
# TODO remove once old shopping list
|
||||||
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
|
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
|
||||||
list_from_recipe(
|
SLR = RecipeShoppingEditor(user=self.context['request'].user, space=self.context['request'].space)
|
||||||
list_recipe=instance,
|
SLR.edit_servings(servings=validated_data['servings'], id=instance.id)
|
||||||
servings=validated_data['servings'],
|
|
||||||
created_by=self.context['request'].user,
|
|
||||||
space=self.context['request'].space
|
|
||||||
)
|
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -6,8 +6,9 @@ from django.contrib.postgres.search import SearchVector
|
|||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
from django_scopes import scope
|
||||||
|
|
||||||
from cookbook.helper.shopping_helper import list_from_recipe
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||||
from cookbook.managers import DICTIONARY
|
from cookbook.managers import DICTIONARY
|
||||||
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
|
from cookbook.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
|
||||||
ShoppingListEntry, Step)
|
ShoppingListEntry, Step)
|
||||||
@ -104,20 +105,31 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
|
|||||||
|
|
||||||
@receiver(post_save, sender=MealPlan)
|
@receiver(post_save, sender=MealPlan)
|
||||||
def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs):
|
def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs):
|
||||||
|
if not instance:
|
||||||
|
return
|
||||||
user = instance.get_owner()
|
user = instance.get_owner()
|
||||||
if not user.userpreference.mealplan_autoadd_shopping:
|
with scope(space=instance.space):
|
||||||
|
slr_exists = instance.shoppinglistrecipe_set.exists()
|
||||||
|
|
||||||
|
if not created and slr_exists:
|
||||||
|
for x in instance.shoppinglistrecipe_set.all():
|
||||||
|
# assuming that permissions checks for the MealPlan have happened upstream
|
||||||
|
if instance.servings != x.servings:
|
||||||
|
SLR = RecipeShoppingEditor(id=x.id, user=user, space=instance.space)
|
||||||
|
SLR.edit_servings(servings=instance.servings)
|
||||||
|
# list_recipe = list_from_recipe(list_recipe=x, servings=instance.servings, space=instance.space)
|
||||||
|
elif not user.userpreference.mealplan_autoadd_shopping or not instance.recipe:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not created and instance.shoppinglistrecipe_set.exists():
|
if created:
|
||||||
for x in instance.shoppinglistrecipe_set.all():
|
|
||||||
if instance.servings != x.servings:
|
|
||||||
list_recipe = list_from_recipe(list_recipe=x, servings=instance.servings, space=instance.space)
|
|
||||||
elif created:
|
|
||||||
# if creating a mealplan - perform shopping list activities
|
# if creating a mealplan - perform shopping list activities
|
||||||
kwargs = {
|
# kwargs = {
|
||||||
'mealplan': instance,
|
# 'mealplan': instance,
|
||||||
'space': instance.space,
|
# 'space': instance.space,
|
||||||
'created_by': user,
|
# 'created_by': user,
|
||||||
'servings': instance.servings
|
# 'servings': instance.servings
|
||||||
}
|
# }
|
||||||
list_recipe = list_from_recipe(**kwargs)
|
SLR = RecipeShoppingEditor(user=user, space=instance.space)
|
||||||
|
SLR.create(mealplan=instance, servings=instance.servings)
|
||||||
|
|
||||||
|
# list_recipe = list_from_recipe(**kwargs)
|
||||||
|
@ -156,6 +156,32 @@ def test_sharing(request, shared, count, sle_2, sle, u1_s1):
|
|||||||
# confirm shared user sees their list and the list that's shared with them
|
# confirm shared user sees their list and the list that's shared with them
|
||||||
assert len(json.loads(r.content)) == count
|
assert len(json.loads(r.content)) == count
|
||||||
|
|
||||||
|
# test shared user can mark complete
|
||||||
|
x = shared_client.patch(
|
||||||
|
reverse(DETAIL_URL, args={sle[0].id}),
|
||||||
|
{'checked': True},
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
r = json.loads(shared_client.get(reverse(LIST_URL)).content)
|
||||||
|
assert len(r) == count
|
||||||
|
# count unchecked entries
|
||||||
|
if not x.status_code == 404:
|
||||||
|
count = count-1
|
||||||
|
assert [x['checked'] for x in r].count(False) == count
|
||||||
|
# test shared user can delete
|
||||||
|
x = shared_client.delete(
|
||||||
|
reverse(
|
||||||
|
DETAIL_URL,
|
||||||
|
args={sle[1].id}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
r = json.loads(shared_client.get(reverse(LIST_URL)).content)
|
||||||
|
assert len(r) == count
|
||||||
|
# count unchecked entries
|
||||||
|
if not x.status_code == 404:
|
||||||
|
count = count-1
|
||||||
|
assert [x['checked'] for x in r].count(False) == count
|
||||||
|
|
||||||
|
|
||||||
def test_completed(sle, u1_s1):
|
def test_completed(sle, u1_s1):
|
||||||
# check 1 entry
|
# check 1 entry
|
||||||
|
@ -164,7 +164,7 @@ def test_shopping_recipe_edit(request, recipe, sle_count, use_mealplan, u1_s1, u
|
|||||||
assert len(r) == sle_count
|
assert len(r) == sle_count
|
||||||
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == sle_count
|
||||||
|
|
||||||
# test removing 2 items from shopping list
|
# test removing 3 items from shopping list
|
||||||
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
|
u2_s1.put(reverse(SHOPPING_RECIPE_URL, args={recipe.id}),
|
||||||
{'list_recipe': list_recipe, 'ingredients': keep_ing},
|
{'list_recipe': list_recipe, 'ingredients': keep_ing},
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
|
@ -41,7 +41,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, Cus
|
|||||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||||
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
|
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
|
||||||
from cookbook.helper.recipe_url_import import get_from_scraper
|
from cookbook.helper.recipe_url_import import get_from_scraper
|
||||||
from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper
|
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
|
||||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
|
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
|
||||||
ImportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook,
|
ImportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook,
|
||||||
RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry,
|
RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry,
|
||||||
@ -153,11 +153,15 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# TODO have this check unaccent search settings or other search preferences?
|
# TODO have this check unaccent search settings or other search preferences?
|
||||||
|
filter = Q(name__icontains=query)
|
||||||
|
if any([self.model.__name__.lower() in x for x in self.request.user.searchpreference.unaccent.values_list('field', flat=True)]):
|
||||||
|
filter |= Q(name__unaccent__icontains=query)
|
||||||
|
|
||||||
self.queryset = (
|
self.queryset = (
|
||||||
self.queryset
|
self.queryset
|
||||||
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
|
||||||
default=Value(0))) # put exact matches at the top of the result set
|
default=Value(0))) # put exact matches at the top of the result set
|
||||||
.filter(name__icontains=query).order_by('-starts', 'name')
|
.filter(filter).order_by('-starts', 'name')
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_at = self.request.query_params.get('updated_at', None)
|
updated_at = self.request.query_params.get('updated_at', None)
|
||||||
@ -644,7 +648,6 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
schema = QueryParamAutoSchema()
|
schema = QueryParamAutoSchema()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
||||||
if self.detail:
|
if self.detail:
|
||||||
self.queryset = self.queryset.filter(space=self.request.space)
|
self.queryset = self.queryset.filter(space=self.request.space)
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
@ -717,16 +720,27 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
|||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
ingredients = request.data.get('ingredients', None)
|
ingredients = request.data.get('ingredients', None)
|
||||||
servings = request.data.get('servings', None)
|
servings = request.data.get('servings', None)
|
||||||
list_recipe = ShoppingListRecipe.objects.filter(id=request.data.get('list_recipe', None)).first()
|
list_recipe = request.data.get('list_recipe', None)
|
||||||
if servings is None:
|
mealplan = request.data.get('mealplan', None)
|
||||||
servings = getattr(list_recipe, 'servings', obj.servings)
|
SLR = RecipeShoppingEditor(request.user, request.space, id=list_recipe, recipe=obj, mealplan=mealplan)
|
||||||
# created_by needs to be sticky to original creator as it is 'their' shopping list
|
|
||||||
# changing shopping list created_by can shift some items to new owner which may not share in the other direction
|
|
||||||
created_by = getattr(ShoppingListEntry.objects.filter(list_recipe=list_recipe).first(), 'created_by', request.user)
|
|
||||||
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
|
||||||
list_from_recipe(list_recipe=list_recipe, recipe=obj, ingredients=ingredients, servings=servings, space=request.space, created_by=created_by)
|
|
||||||
|
|
||||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||||
|
http_status = status.HTTP_204_NO_CONTENT
|
||||||
|
if servings and servings <= 0:
|
||||||
|
result = SLR.delete()
|
||||||
|
elif list_recipe:
|
||||||
|
result = SLR.edit(servings=servings, ingredients=ingredients)
|
||||||
|
else:
|
||||||
|
result = SLR.create(servings=servings, ingredients=ingredients)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
content = {'msg': ('An error occurred')}
|
||||||
|
http_status = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
else:
|
||||||
|
content = {'msg': _(f'{obj.name} was added to the shopping list.')}
|
||||||
|
http_status = status.HTTP_204_NO_CONTENT
|
||||||
|
|
||||||
|
return Response(content, status=http_status)
|
||||||
|
|
||||||
@decorators.action(
|
@decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
|
@ -54,20 +54,14 @@
|
|||||||
<div class="col-12 col-md-3 calender-options">
|
<div class="col-12 col-md-3 calender-options">
|
||||||
<h5>{{ $t("Planner_Settings") }}</h5>
|
<h5>{{ $t("Planner_Settings") }}</h5>
|
||||||
<b-form>
|
<b-form>
|
||||||
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')"
|
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')" label-for="UomInput">
|
||||||
label-for="UomInput">
|
<b-form-select id="UomInput" v-model="settings.displayPeriodUom" :options="options.displayPeriodUom"></b-form-select>
|
||||||
<b-form-select id="UomInput" v-model="settings.displayPeriodUom"
|
|
||||||
:options="options.displayPeriodUom"></b-form-select>
|
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group id="PeriodInput" :label="$t('Periods')"
|
<b-form-group id="PeriodInput" :label="$t('Periods')" :description="$t('Plan_Show_How_Many_Periods')" label-for="PeriodInput">
|
||||||
:description="$t('Plan_Show_How_Many_Periods')" label-for="PeriodInput">
|
<b-form-select id="PeriodInput" v-model="settings.displayPeriodCount" :options="options.displayPeriodCount"></b-form-select>
|
||||||
<b-form-select id="PeriodInput" v-model="settings.displayPeriodCount"
|
|
||||||
:options="options.displayPeriodCount"></b-form-select>
|
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')"
|
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')" label-for="DaysInput">
|
||||||
label-for="DaysInput">
|
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek" :options="dayNames"></b-form-select>
|
||||||
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek"
|
|
||||||
:options="dayNames"></b-form-select>
|
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group id="WeekNumInput" :label="$t('Week_Numbers')">
|
<b-form-group id="WeekNumInput" :label="$t('Week_Numbers')">
|
||||||
<b-form-checkbox v-model="settings.displayWeekNumbers" name="week_num">
|
<b-form-checkbox v-model="settings.displayWeekNumbers" name="week_num">
|
||||||
@ -80,23 +74,18 @@
|
|||||||
<h5>{{ $t("Meal_Types") }}</h5>
|
<h5>{{ $t("Meal_Types") }}</h5>
|
||||||
<div>
|
<div>
|
||||||
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" @sort="sortMealTypes()" ghost-class="ghost">
|
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" @sort="sortMealTypes()" ghost-class="ghost">
|
||||||
<b-card no-body class="mt-1 list-group-item p-2" style="cursor:move" v-for="(meal_type, index) in meal_types" v-hover
|
<b-card no-body class="mt-1 list-group-item p-2" style="cursor: move" v-for="(meal_type, index) in meal_types" v-hover :key="meal_type.id">
|
||||||
:key="meal_type.id">
|
|
||||||
<b-card-header class="p-2 border-0">
|
<b-card-header class="p-2 border-0">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<button type="button" class="btn btn-lg shadow-none"><i
|
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||||
class="fas fa-arrows-alt-v"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<h5 class="mt-1 mb-1">
|
<h5 class="mt-1 mb-1">
|
||||||
{{ meal_type.icon }} {{
|
{{ meal_type.icon }} {{ meal_type.name
|
||||||
meal_type.name
|
}}<span class="float-right text-primary" style="cursor: pointer"
|
||||||
}}<span class="float-right text-primary" style="cursor:pointer"
|
><i class="fa" v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }" @click="editOrSaveMealType(index)" aria-hidden="true"></i
|
||||||
><i class="fa"
|
></span>
|
||||||
v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }"
|
|
||||||
@click="editOrSaveMealType(index)" aria-hidden="true"></i
|
|
||||||
></span>
|
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -104,26 +93,19 @@
|
|||||||
<b-card-body class="p-4" v-if="meal_type.editing">
|
<b-card-body class="p-4" v-if="meal_type.editing">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ $t("Name") }}</label>
|
<label>{{ $t("Name") }}</label>
|
||||||
<input class="form-control" placeholder="Name" v-model="meal_type.name"/>
|
<input class="form-control" placeholder="Name" v-model="meal_type.name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<emoji-input :field="'icon'" :label="$t('Icon')"
|
<emoji-input :field="'icon'" :label="$t('Icon')" :value="meal_type.icon"></emoji-input>
|
||||||
:value="meal_type.icon"></emoji-input>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ $t("Color") }}</label>
|
<label>{{ $t("Color") }}</label>
|
||||||
<input class="form-control" type="color" name="Name"
|
<input class="form-control" type="color" name="Name" :value="meal_type.color" @change="meal_type.color = $event.target.value" />
|
||||||
:value="meal_type.color"
|
|
||||||
@change="meal_type.color = $event.target.value"/>
|
|
||||||
</div>
|
</div>
|
||||||
<b-form-checkbox id="checkbox-1" v-model="meal_type.default"
|
<b-form-checkbox id="checkbox-1" v-model="meal_type.default" name="default_checkbox" class="mb-2">
|
||||||
name="default_checkbox" class="mb-2">
|
|
||||||
{{ $t("Default") }}
|
{{ $t("Default") }}
|
||||||
</b-form-checkbox>
|
</b-form-checkbox>
|
||||||
<button class="btn btn-danger" @click="deleteMealType(index)">{{
|
<button class="btn btn-danger" @click="deleteMealType(index)">{{ $t("Delete") }}</button>
|
||||||
$t("Delete")
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">
|
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">
|
||||||
{{ $t("Save") }}
|
{{ $t("Save") }}
|
||||||
</button>
|
</button>
|
||||||
@ -147,15 +129,16 @@
|
|||||||
openEntryEdit(contextData.originalItem.entry)
|
openEntryEdit(contextData.originalItem.entry)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{
|
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{ $t("Edit") }}</a>
|
||||||
$t("Edit")
|
|
||||||
}}</a>
|
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
v-if="contextData.originalItem.entry.recipe != null"
|
v-if="contextData && contextData.originalItem && contextData.originalItem.entry.recipe != null"
|
||||||
@click="$refs.menu.close();openRecipe(contextData.originalItem.entry.recipe)">
|
@click="
|
||||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pizza-slice"></i>
|
$refs.menu.close()
|
||||||
{{ $t("Recipe") }}</a>
|
openRecipe(contextData.originalItem.entry.recipe)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pizza-slice"></i> {{ $t("Recipe") }}</a>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
@click="
|
@click="
|
||||||
@ -163,8 +146,7 @@
|
|||||||
moveEntryLeft(contextData)
|
moveEntryLeft(contextData)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i>
|
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i> {{ $t("Move") }}</a>
|
||||||
{{ $t("Move") }}</a>
|
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
@click="
|
@click="
|
||||||
@ -172,8 +154,7 @@
|
|||||||
moveEntryRight(contextData)
|
moveEntryRight(contextData)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i>
|
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i> {{ $t("Move") }}</a>
|
||||||
{{ $t("Move") }}</a>
|
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
@click="
|
@click="
|
||||||
@ -189,8 +170,7 @@
|
|||||||
addToShopping(contextData)
|
addToShopping(contextData)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i>
|
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
|
||||||
{{ $t("Add_to_Shopping") }}</a>
|
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
@click="
|
@click="
|
||||||
@ -198,15 +178,12 @@
|
|||||||
deleteEntry(contextData)
|
deleteEntry(contextData)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i>
|
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||||
{{ $t("Delete") }}</a>
|
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</template>
|
</template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<meal-plan-edit-modal
|
<meal-plan-edit-modal
|
||||||
:entry="entryEditing"
|
:entry="entryEditing"
|
||||||
:entryEditing_initial_recipe="entryEditing_initial_recipe"
|
|
||||||
:entry-editing_initial_meal_type="entryEditing_initial_meal_type"
|
|
||||||
:modal_title="modal_title"
|
:modal_title="modal_title"
|
||||||
:edit_modal_show="edit_modal_show"
|
:edit_modal_show="edit_modal_show"
|
||||||
@save-entry="editEntry"
|
@save-entry="editEntry"
|
||||||
@ -230,10 +207,11 @@
|
|||||||
<div class="col-12 mt-1" v-if="shopping_list.length > 0">
|
<div class="col-12 mt-1" v-if="shopping_list.length > 0">
|
||||||
<b-button-group>
|
<b-button-group>
|
||||||
<b-button variant="success" @click="saveShoppingList"
|
<b-button variant="success" @click="saveShoppingList"
|
||||||
><i class="fas fa-external-link-alt"></i>
|
><i class="fas fa-external-link-alt"></i>
|
||||||
{{ $t("Open") }}
|
{{ $t("Open") }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="danger" @click="shopping_list = []"><i class="fa fa-trash"></i>
|
<b-button variant="danger" @click="shopping_list = []"
|
||||||
|
><i class="fa fa-trash"></i>
|
||||||
{{ $t("Clear") }}
|
{{ $t("Clear") }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
@ -243,46 +221,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<transition name="slide-fade">
|
<transition name="slide-fade">
|
||||||
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)"
|
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
||||||
v-if="current_tab === 0">
|
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
|
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i class="fas fa-calendar-plus"></i> {{ $t("Create") }}</button>
|
||||||
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i
|
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i class="fas fa-shopping-cart"></i> {{ $t("Shopping_list") }}</button>
|
||||||
class="fas fa-shopping-cart"></i> {{ $t("Shopping_list") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
|
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
|
||||||
><i class="fas fa-download"></i>
|
><i class="fas fa-download"></i>
|
||||||
{{ $t("Export_To_ICal") }}
|
{{ $t("Export_To_ICal") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-md-3 col-6">
|
||||||
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top
|
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top :title="$t('Coming_Soon')">
|
||||||
:title="$t('Coming_Soon')">
|
|
||||||
{{ $t("Auto_Planner") }}
|
{{ $t("Auto_Planner") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
|
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
|
||||||
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
|
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1">
|
||||||
<b-button v-html="'<<'"
|
<b-button v-html="'<<'" @click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
|
||||||
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
|
|
||||||
<b-button v-html="'<'" @click="setStartingDay(-1)"></b-button>
|
<b-button v-html="'<'" @click="setStartingDay(-1)"></b-button>
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1">
|
||||||
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
|
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i class="fas fa-home"></i></b-button>
|
||||||
class="fas fa-home"></i></b-button>
|
|
||||||
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
|
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
<b-button-group class="mx-1">
|
<b-button-group class="mx-1">
|
||||||
<b-button v-html="'>'" @click="setStartingDay(1)"></b-button>
|
<b-button v-html="'>'" @click="setStartingDay(1)"></b-button>
|
||||||
<b-button v-html="'>>'"
|
<b-button v-html="'>>'" @click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
|
||||||
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
|
|
||||||
</b-button-group>
|
</b-button-group>
|
||||||
</b-button-toolbar>
|
</b-button-toolbar>
|
||||||
</div>
|
</div>
|
||||||
@ -293,7 +262,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
import {BootstrapVue} from "bootstrap-vue"
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
|
|
||||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||||
@ -307,11 +276,11 @@ import moment from "moment"
|
|||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
import VueCookies from "vue-cookies"
|
import VueCookies from "vue-cookies"
|
||||||
|
|
||||||
import {ApiMixin, StandardToasts, ResolveUrlMixin} from "@/utils/utils"
|
import { ApiMixin, StandardToasts, ResolveUrlMixin } from "@/utils/utils"
|
||||||
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle"
|
import { CalendarView, CalendarMathMixin } from "vue-simple-calendar/src/components/bundle"
|
||||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||||
|
|
||||||
const {makeToast} = require("@/utils/utils")
|
const { makeToast } = require("@/utils/utils")
|
||||||
|
|
||||||
Vue.prototype.moment = moment
|
Vue.prototype.moment = moment
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
@ -349,12 +318,12 @@ export default {
|
|||||||
current_context_menu_item: null,
|
current_context_menu_item: null,
|
||||||
options: {
|
options: {
|
||||||
displayPeriodUom: [
|
displayPeriodUom: [
|
||||||
{text: this.$t("Week"), value: "week"},
|
{ text: this.$t("Week"), value: "week" },
|
||||||
{
|
{
|
||||||
text: this.$t("Month"),
|
text: this.$t("Month"),
|
||||||
value: "month",
|
value: "month",
|
||||||
},
|
},
|
||||||
{text: this.$t("Year"), value: "year"},
|
{ text: this.$t("Year"), value: "year" },
|
||||||
],
|
],
|
||||||
displayPeriodCount: [1, 2, 3],
|
displayPeriodCount: [1, 2, 3],
|
||||||
entryEditing: {
|
entryEditing: {
|
||||||
@ -385,20 +354,6 @@ export default {
|
|||||||
return this.$t("Edit_Meal_Plan_Entry")
|
return this.$t("Edit_Meal_Plan_Entry")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
entryEditing_initial_recipe: function () {
|
|
||||||
if (this.entryEditing.recipe != null) {
|
|
||||||
return [this.entryEditing.recipe]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
entryEditing_initial_meal_type: function () {
|
|
||||||
if (this.entryEditing.meal_type != null) {
|
|
||||||
return [this.entryEditing.meal_type]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plan_items: function () {
|
plan_items: function () {
|
||||||
let items = []
|
let items = []
|
||||||
this.plan_entries.forEach((entry) => {
|
this.plan_entries.forEach((entry) => {
|
||||||
@ -412,7 +367,7 @@ export default {
|
|||||||
dayNames: function () {
|
dayNames: function () {
|
||||||
let options = []
|
let options = []
|
||||||
this.getFormattedWeekdayNames(this.userLocale, "long", 0).forEach((day, index) => {
|
this.getFormattedWeekdayNames(this.userLocale, "long", 0).forEach((day, index) => {
|
||||||
options.push({text: day, value: index})
|
options.push({ text: day, value: index })
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
},
|
},
|
||||||
@ -455,7 +410,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openRecipe: function (recipe) {
|
openRecipe: function (recipe) {
|
||||||
window.open(this.resolveDjangoUrl('view_recipe', recipe.id))
|
window.open(this.resolveDjangoUrl("view_recipe", recipe.id))
|
||||||
},
|
},
|
||||||
addToShopping(entry) {
|
addToShopping(entry) {
|
||||||
if (entry.originalItem.entry.recipe !== null) {
|
if (entry.originalItem.entry.recipe !== null) {
|
||||||
@ -491,7 +446,7 @@ export default {
|
|||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
apiClient
|
apiClient
|
||||||
.createMealType({name: this.$t("Meal_Type")})
|
.createMealType({ name: this.$t("Meal_Type") })
|
||||||
.then((e) => {
|
.then((e) => {
|
||||||
this.periodChangedCallback(this.current_period)
|
this.periodChangedCallback(this.current_period)
|
||||||
})
|
})
|
||||||
@ -879,7 +834,7 @@ having to override as much.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background: #c8ebfb;
|
background: #c8ebfb;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -161,7 +161,7 @@
|
|||||||
:flat="true"
|
:flat="true"
|
||||||
:auto-load-root-options="false"
|
:auto-load-root-options="false"
|
||||||
searchNested
|
searchNested
|
||||||
:placeholder="$t('Ingredients')"
|
:placeholder="$t('Foods')"
|
||||||
:normalizer="normalizer"
|
:normalizer="normalizer"
|
||||||
@input="refreshData(false)"
|
@input="refreshData(false)"
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
@ -457,7 +457,7 @@ export default {
|
|||||||
this.pagination_count = result.data.count
|
this.pagination_count = result.data.count
|
||||||
|
|
||||||
this.facets = result.data.facets
|
this.facets = result.data.facets
|
||||||
this.recipes = this.removeDuplicates(result.data.results, (recipe) => recipe.id)
|
this.recipes = [...this.removeDuplicates(result.data.results, (recipe) => recipe.id)]
|
||||||
if (!this.searchFiltered()) {
|
if (!this.searchFiltered()) {
|
||||||
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
||||||
let mealPlans = []
|
let mealPlans = []
|
||||||
|
@ -93,6 +93,7 @@
|
|||||||
:servings="servings"
|
:servings="servings"
|
||||||
:header="true"
|
:header="true"
|
||||||
@checked-state-changed="updateIngredientCheckedState"
|
@checked-state-changed="updateIngredientCheckedState"
|
||||||
|
@change-servings="servings = $event"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -271,8 +272,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app > div > div{
|
#app > div > div {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
||||||
<!-- passing an array of values to the table grouped by Food -->
|
<!-- passing an array of values to the table grouped by Food -->
|
||||||
<transition-group name="slide-fade">
|
<transition-group name="slide-fade">
|
||||||
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
<div class="mx-4" v-for="(entries, x) in Object.entries(s)" :key="x">
|
||||||
<transition name="slide-fade" mode="out-in">
|
<transition name="slide-fade" mode="out-in">
|
||||||
<ShoppingLineItem
|
<ShoppingLineItem
|
||||||
:entries="entries[1]"
|
:entries="entries[1]"
|
||||||
@ -190,6 +190,9 @@
|
|||||||
<td class="block-inline">
|
<td class="block-inline">
|
||||||
<b-form-input min="1" type="number" :debounce="300" :value="r.recipe_mealplan.servings" @input="updateServings($event, r.list_recipe)"></b-form-input>
|
<b-form-input min="1" type="number" :debounce="300" :value="r.recipe_mealplan.servings" @input="updateServings($event, r.list_recipe)"></b-form-input>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<i class="btn text-primary far fa-eye fa-lg px-2 border-0" variant="link" :title="$t('view_recipe')" @click="editRecipeList($event, r)" />
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<i class="btn text-danger fas fa-trash fa-lg px-2 border-0" variant="link" :title="$t('Delete')" @click="deleteRecipe($event, r.list_recipe)" />
|
<i class="btn text-danger fas fa-trash fa-lg px-2 border-0" variant="link" :title="$t('Delete')" @click="deleteRecipe($event, r.list_recipe)" />
|
||||||
</td>
|
</td>
|
||||||
@ -432,7 +435,10 @@
|
|||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<generic-multiselect
|
<generic-multiselect
|
||||||
size="sm"
|
size="sm"
|
||||||
@change="settings.shopping_share = $event.val;saveSettings()"
|
@change="
|
||||||
|
settings.shopping_share = $event.val
|
||||||
|
saveSettings()
|
||||||
|
"
|
||||||
:model="Models.USER"
|
:model="Models.USER"
|
||||||
:initial_selection="settings.shopping_share"
|
:initial_selection="settings.shopping_share"
|
||||||
label="username"
|
label="username"
|
||||||
@ -557,6 +563,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
|
|
||||||
|
<transition name="slided-fade">
|
||||||
|
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
||||||
|
<div class="col-6">
|
||||||
|
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode"
|
||||||
|
><i class="fas fa-cart-plus"></i>
|
||||||
|
{{ $t("New Entry") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<b-dropdown id="dropdown-dropup" block dropup variant="primary" class="shadow-none">
|
||||||
|
<template #button-content> <i class="fas fa-download"></i> {{ $t("Export") }} </template>
|
||||||
|
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||||
|
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
||||||
|
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')" icon="fas fa-clipboard-list" />
|
||||||
|
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')" icon="fab fa-markdown" />
|
||||||
|
</b-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
|
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
|
||||||
<div>
|
<div>
|
||||||
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
|
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
|
||||||
@ -637,26 +663,7 @@
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</template>
|
</template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<transition name="slided-fade">
|
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)" :modal_id="new_recipe.id" @finish="finishShopping" :list_recipe="new_recipe.list_recipe" />
|
||||||
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
|
||||||
<div class="col-6">
|
|
||||||
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode"
|
|
||||||
><i class="fas fa-cart-plus"></i>
|
|
||||||
{{ $t("New Entry") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<b-dropdown id="dropdown-dropup" block dropup variant="primary" class="shadow-none">
|
|
||||||
<template #button-content> <i class="fas fa-download"></i> {{ $t("Export") }} </template>
|
|
||||||
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
|
||||||
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
|
||||||
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')" icon="fas fa-clipboard-list" />
|
|
||||||
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')" icon="fab fa-markdown" />
|
|
||||||
</b-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)" :modal_id="new_recipe.id" @finish="finishShopping" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -908,6 +915,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
console.log(screen.height)
|
||||||
this.getShoppingList()
|
this.getShoppingList()
|
||||||
this.getSupermarkets()
|
this.getSupermarkets()
|
||||||
this.getShoppingCategories()
|
this.getShoppingCategories()
|
||||||
@ -1096,7 +1104,6 @@ export default {
|
|||||||
if (!autosync) {
|
if (!autosync) {
|
||||||
if (results.data?.length) {
|
if (results.data?.length) {
|
||||||
this.items = results.data
|
this.items = results.data
|
||||||
console.log(this.items)
|
|
||||||
} else {
|
} else {
|
||||||
console.log("no data returned")
|
console.log("no data returned")
|
||||||
}
|
}
|
||||||
@ -1399,11 +1406,23 @@ export default {
|
|||||||
window.removeEventListener("offline", this.updateOnlineStatus)
|
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||||
},
|
},
|
||||||
addRecipeToShopping() {
|
addRecipeToShopping() {
|
||||||
|
console.log(this.new_recipe)
|
||||||
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
||||||
},
|
},
|
||||||
finishShopping() {
|
finishShopping() {
|
||||||
|
this.add_recipe_servings = 1
|
||||||
|
this.new_recipe = { id: undefined }
|
||||||
|
this.edit_recipe_list = undefined
|
||||||
this.getShoppingList()
|
this.getShoppingList()
|
||||||
},
|
},
|
||||||
|
editRecipeList(e, r) {
|
||||||
|
this.new_recipe = { id: r.recipe_mealplan.recipe, name: r.recipe_mealplan.recipe_name, servings: r.recipe_mealplan.servings, list_recipe: r.list_recipe }
|
||||||
|
this.$nextTick(function () {
|
||||||
|
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
hover: {
|
hover: {
|
||||||
@ -1465,14 +1484,25 @@ export default {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
#shoppinglist {
|
#shoppinglist {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
height: 65vh;
|
height: 6vh;
|
||||||
|
padding-right: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (min-height: 700px) and (max-width: 768px) {
|
||||||
|
#shoppinglist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 72vh;
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
v-model="selected_objects"
|
v-model="selected_objects"
|
||||||
:options="objects"
|
:options="objects"
|
||||||
:close-on-select="true"
|
:close-on-select="true"
|
||||||
:clear-on-select="true"
|
:clear-on-select="multiple"
|
||||||
:hide-selected="multiple"
|
:hide-selected="multiple"
|
||||||
:preserve-search="true"
|
:preserve-search="true"
|
||||||
:internal-search="false"
|
:internal-search="false"
|
||||||
@ -35,7 +35,7 @@ export default {
|
|||||||
// this.Models and this.Actions inherited from ApiMixin
|
// this.Models and this.Actions inherited from ApiMixin
|
||||||
loading: false,
|
loading: false,
|
||||||
objects: [],
|
objects: [],
|
||||||
selected_objects: [],
|
selected_objects: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -48,7 +48,7 @@ export default {
|
|||||||
},
|
},
|
||||||
label: { type: String, default: "name" },
|
label: { type: String, default: "name" },
|
||||||
parent_variable: { type: String, default: undefined },
|
parent_variable: { type: String, default: undefined },
|
||||||
limit: { type: Number, default: 10 },
|
limit: { type: Number, default: 25 },
|
||||||
sticky_options: {
|
sticky_options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default() {
|
default() {
|
||||||
@ -61,6 +61,10 @@ export default {
|
|||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
initial_single_selection: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
multiple: { type: Boolean, default: true },
|
multiple: { type: Boolean, default: true },
|
||||||
allow_create: { type: Boolean, default: false },
|
allow_create: { type: Boolean, default: false },
|
||||||
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
|
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
|
||||||
@ -71,18 +75,37 @@ export default {
|
|||||||
// watch it
|
// watch it
|
||||||
this.selected_objects = newVal
|
this.selected_objects = newVal
|
||||||
},
|
},
|
||||||
|
initial_single_selection: function (newVal, oldVal) {
|
||||||
|
// watch it
|
||||||
|
this.selected_objects = newVal
|
||||||
|
},
|
||||||
clear: function (newVal, oldVal) {
|
clear: function (newVal, oldVal) {
|
||||||
this.selected_objects = []
|
if (this.multiple || !this.initial_single_selection) {
|
||||||
|
this.selected_objects = []
|
||||||
|
} else {
|
||||||
|
this.selected_objects = undefined
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.search("")
|
this.search("")
|
||||||
this.selected_objects = this.initial_selection
|
if (this.multiple || !this.initial_single_selection) {
|
||||||
|
this.selected_objects = this.initial_selection
|
||||||
|
} else {
|
||||||
|
this.selected_objects = this.initial_single_selection
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lookupPlaceholder() {
|
lookupPlaceholder() {
|
||||||
return this.placeholder || this.model.name || this.$t("Search")
|
return this.placeholder || this.model.name || this.$t("Search")
|
||||||
},
|
},
|
||||||
|
nothingSelected() {
|
||||||
|
if (this.multiple || !this.initial_single_selection) {
|
||||||
|
return this.selected_objects.length === 0 && this.initial_selection.length === 0
|
||||||
|
} else {
|
||||||
|
return !this.selected_objects && !this.initial_single_selection
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
@ -95,8 +118,9 @@ export default {
|
|||||||
}
|
}
|
||||||
this.genericAPI(this.model, this.Actions.LIST, options).then((result) => {
|
this.genericAPI(this.model, this.Actions.LIST, options).then((result) => {
|
||||||
this.objects = this.sticky_options.concat(result.data?.results ?? result.data)
|
this.objects = this.sticky_options.concat(result.data?.results ?? result.data)
|
||||||
if (this.selected_objects.length === 0 && this.initial_selection.length === 0 && this.objects.length > 0) {
|
if (this.nothingSelected && this.objects.length > 0) {
|
||||||
this.objects.forEach((item) => {
|
this.objects.forEach((item) => {
|
||||||
|
// select default items when present in object
|
||||||
if ("default" in item) {
|
if ("default" in item) {
|
||||||
if (item.default) {
|
if (item.default) {
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
@ -109,6 +133,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// this.removeMissingItems() # This removes items that are on another page of results
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
selectionChanged: function () {
|
selectionChanged: function () {
|
||||||
@ -121,6 +146,13 @@ export default {
|
|||||||
this.search("")
|
this.search("")
|
||||||
}, 750)
|
}, 750)
|
||||||
},
|
},
|
||||||
|
// removeMissingItems: function () {
|
||||||
|
// if (this.multiple) {
|
||||||
|
// this.selected_objects = this.selected_objects.filter((x) => !this.objects.map((y) => y.id).includes(x))
|
||||||
|
// } else {
|
||||||
|
// this.selected_objects = this.objects.filter((x) => x.id === this.selected_objects.id)[0]
|
||||||
|
// }
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<td class="d-print-non" v-if="detailed && !add_shopping_mode" @click="done">
|
<td class="d-print-non" v-if="detailed && !show_shopping" @click="done">
|
||||||
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
|
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
|
||||||
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
|
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
|
||||||
</td>
|
</td>
|
||||||
@ -39,9 +39,9 @@
|
|||||||
variant="link"
|
variant="link"
|
||||||
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
|
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
|
||||||
:class="{
|
:class="{
|
||||||
'text-success': shopping_status === true,
|
'text-success': ingredient.shopping_status === true,
|
||||||
'text-muted': shopping_status === false,
|
'text-muted': ingredient.shopping_status === false,
|
||||||
'text-warning': shopping_status === null,
|
'text-warning': ingredient.shopping_status === null,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<span v-if="!ingredient.food.ignore_shopping" class="px-2">
|
<span v-if="!ingredient.food.ignore_shopping" class="px-2">
|
||||||
@ -64,111 +64,49 @@ export default {
|
|||||||
ingredient: Object,
|
ingredient: Object,
|
||||||
ingredient_factor: { type: Number, default: 1 },
|
ingredient_factor: { type: Number, default: 1 },
|
||||||
detailed: { type: Boolean, default: true },
|
detailed: { type: Boolean, default: true },
|
||||||
recipe_list: { type: Number }, // ShoppingListRecipe ID, to filter ShoppingStatus
|
|
||||||
show_shopping: { type: Boolean, default: false },
|
show_shopping: { type: Boolean, default: false },
|
||||||
add_shopping_mode: { type: Boolean, default: false },
|
|
||||||
shopping_list: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
}, // list of unchecked ingredients in shopping list
|
|
||||||
},
|
},
|
||||||
mixins: [ResolveUrlMixin, ApiMixin],
|
mixins: [ResolveUrlMixin, ApiMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
checked: false,
|
checked: false,
|
||||||
shopping_status: null, // in any shopping list: boolean + null=in shopping list, but not for this recipe
|
|
||||||
shopping_items: [],
|
|
||||||
shop: false, // in shopping list for this recipe: boolean
|
shop: false, // in shopping list for this recipe: boolean
|
||||||
dirty: undefined,
|
dirty: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ShoppingListAndFilter: {
|
ingredient: {
|
||||||
immediate: true,
|
handler() {},
|
||||||
handler(newVal, oldVal) {
|
deep: true,
|
||||||
// this whole sections is overly complicated
|
},
|
||||||
// trying to infer status of shopping for THIS recipe and THIS ingredient
|
"ingredient.shop": function (newVal) {
|
||||||
// without know which recipe it is.
|
this.shop = newVal
|
||||||
// If refactored:
|
|
||||||
// ## Needs to handle same recipe (multiple mealplans) being in shopping list multiple times
|
|
||||||
// ## Needs to handle same recipe being added as ShoppingListRecipe AND ingredients added from recipe as one-off
|
|
||||||
|
|
||||||
let filtered_list = this.shopping_list
|
|
||||||
// if a recipe list is provided, filter the shopping list
|
|
||||||
if (this.recipe_list) {
|
|
||||||
filtered_list = filtered_list.filter((x) => x.list_recipe == this.recipe_list)
|
|
||||||
}
|
|
||||||
// how many ShoppingListRecipes are there for this recipe?
|
|
||||||
let count_shopping_recipes = [...new Set(filtered_list.filter((x) => x.list_recipe))].length
|
|
||||||
let count_shopping_ingredient = filtered_list.filter((x) => x.ingredient == this.ingredient.id).length
|
|
||||||
|
|
||||||
if (count_shopping_recipes >= 1 && this.recipe_list) {
|
|
||||||
// This recipe is in the shopping list
|
|
||||||
this.shop = false // don't check any boxes until user selects a shopping list to edit
|
|
||||||
if (count_shopping_ingredient >= 1) {
|
|
||||||
this.shopping_status = true // ingredient is in the shopping list - probably (but not definitely, this ingredient)
|
|
||||||
} else if (this.ingredient?.food?.shopping) {
|
|
||||||
this.shopping_status = null // food is in the shopping list, just not for this ingredient/recipe
|
|
||||||
} else {
|
|
||||||
// food is not in any shopping list
|
|
||||||
this.shopping_status = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// there are not recipes in the shopping list
|
|
||||||
// set default value
|
|
||||||
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe && !this.ingredient?.food?.ignore_shopping
|
|
||||||
this.$emit("add-to-shopping", { item: this.ingredient, add: this.shop })
|
|
||||||
// mark checked if the food is in the shopping list for this ingredient/recipe
|
|
||||||
if (count_shopping_ingredient >= 1) {
|
|
||||||
// ingredient is in this shopping list (not entirely sure how this could happen?)
|
|
||||||
this.shopping_status = true
|
|
||||||
} else if (count_shopping_ingredient == 0 && this.ingredient?.food?.shopping) {
|
|
||||||
// food is in the shopping list, just not for this ingredient/recipe
|
|
||||||
this.shopping_status = null
|
|
||||||
} else {
|
|
||||||
// the food is not in any shopping list
|
|
||||||
this.shopping_status = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.add_shopping_mode) {
|
|
||||||
// if we are in add shopping mode (e.g. recipe_shopping_modal) start with all checks marked
|
|
||||||
// except if on_hand (could be if recipe too?)
|
|
||||||
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe && !this.ingredient?.food?.ignore_shopping
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {
|
||||||
|
this.shop = this.ingredient?.shop
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ShoppingListAndFilter() {
|
|
||||||
// hack to watch the shopping list and the recipe list at the same time
|
|
||||||
return this.shopping_list.map((x) => x.id).join(this.recipe_list)
|
|
||||||
},
|
|
||||||
ShoppingPopover() {
|
ShoppingPopover() {
|
||||||
if (this.shopping_status == false) {
|
if (this.ingredient?.shopping_status == false) {
|
||||||
return this.$t("NotInShopping", { food: this.ingredient.food.name })
|
return this.$t("NotInShopping", { food: this.ingredient.food.name })
|
||||||
} else {
|
} else {
|
||||||
let list = this.shopping_list.filter((x) => x.food.id == this.ingredient.food.id)
|
let category = this.$t("Category") + ": " + this.ingredient?.category ?? this.$t("Undefined")
|
||||||
let category = this.$t("Category") + ": " + this.ingredient?.food?.supermarket_category?.name ?? this.$t("Undefined")
|
|
||||||
let popover = []
|
let popover = []
|
||||||
|
;(this.ingredient?.shopping_list ?? []).forEach((x) => {
|
||||||
list.forEach((x) => {
|
|
||||||
popover.push(
|
popover.push(
|
||||||
[
|
[
|
||||||
"<tr style='border-bottom: 1px solid #ccc'>",
|
"<tr style='border-bottom: 1px solid #ccc'>",
|
||||||
"<td style='padding: 3px;'><em>",
|
"<td style='padding: 3px;'><em>",
|
||||||
x?.recipe_mealplan?.name ?? "",
|
x?.mealplan ?? "",
|
||||||
"</em></td>",
|
"</em></td>",
|
||||||
"<td style='padding: 3px;'>",
|
"<td style='padding: 3px;'>",
|
||||||
x?.amount ?? "",
|
x?.amount ?? "",
|
||||||
"</td>",
|
"</td>",
|
||||||
"<td style='padding: 3px;'>",
|
"<td style='padding: 3px;'>",
|
||||||
x?.unit?.name ?? "" + "</td>",
|
x?.unit ?? "" + "</td>",
|
||||||
"<td style='padding: 3px;'>",
|
"<td style='padding: 3px;'>",
|
||||||
x?.food?.name ?? "",
|
x?.food ?? "",
|
||||||
"</td></tr>",
|
"</td></tr>",
|
||||||
].join("")
|
].join("")
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row text-right" v-if="ShoppingRecipes.length > 1">
|
<div class="row text-right" v-if="ShoppingRecipes.length > 1 && !add_shopping_mode">
|
||||||
<div class="col col-md-6 offset-md-6 text-right">
|
<div class="col col-md-6 offset-md-6 text-right">
|
||||||
<b-form-select v-model="selected_shoppingrecipe" :options="ShoppingRecipes" size="sm"></b-form-select>
|
<b-form-select v-model="selected_shoppingrecipe" :options="ShoppingRecipes" size="sm"></b-form-select>
|
||||||
</div>
|
</div>
|
||||||
@ -31,14 +31,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<template v-for="i in s.ingredients">
|
<template v-for="i in s.ingredients">
|
||||||
<ingredient-component
|
<ingredient-component
|
||||||
:ingredient="i"
|
:ingredient="prepareIngredient(i)"
|
||||||
:ingredient_factor="ingredient_factor"
|
:ingredient_factor="ingredient_factor"
|
||||||
:key="i.id"
|
:key="i.id"
|
||||||
:show_shopping="show_shopping"
|
:show_shopping="show_shopping"
|
||||||
:shopping_list="shopping_list"
|
|
||||||
:add_shopping_mode="add_shopping_mode"
|
|
||||||
:detailed="detailed"
|
:detailed="detailed"
|
||||||
:recipe_list="selected_shoppingrecipe"
|
|
||||||
@checked-state-changed="$emit('checked-state-changed', $event)"
|
@checked-state-changed="$emit('checked-state-changed', $event)"
|
||||||
@add-to-shopping="addShopping($event)"
|
@add-to-shopping="addShopping($event)"
|
||||||
/>
|
/>
|
||||||
@ -59,6 +56,7 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
|
|||||||
|
|
||||||
import IngredientComponent from "@/components/IngredientComponent"
|
import IngredientComponent from "@/components/IngredientComponent"
|
||||||
import { ApiMixin, StandardToasts } from "@/utils/utils"
|
import { ApiMixin, StandardToasts } from "@/utils/utils"
|
||||||
|
import ShoppingListViewVue from "../apps/ShoppingListView/ShoppingListView.vue"
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
@ -79,6 +77,7 @@ export default {
|
|||||||
detailed: { type: Boolean, default: true },
|
detailed: { type: Boolean, default: true },
|
||||||
header: { type: Boolean, default: false },
|
header: { type: Boolean, default: false },
|
||||||
add_shopping_mode: { type: Boolean, default: false },
|
add_shopping_mode: { type: Boolean, default: false },
|
||||||
|
recipe_list: { type: Number, default: undefined },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -107,13 +106,14 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
ShoppingRecipes: function (newVal, oldVal) {
|
ShoppingRecipes: function (newVal, oldVal) {
|
||||||
if (newVal.length === 0 || this.add_shopping_mode) {
|
if (newVal.length === 0 || this.add_shopping_mode) {
|
||||||
this.selected_shoppingrecipe = undefined
|
this.selected_shoppingrecipe = this.recipe_list
|
||||||
} else if (newVal.length === 1) {
|
} else if (newVal.length === 1) {
|
||||||
this.selected_shoppingrecipe = newVal[0].value
|
this.selected_shoppingrecipe = newVal[0].value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selected_shoppingrecipe: function (newVal, oldVal) {
|
selected_shoppingrecipe: function (newVal, oldVal) {
|
||||||
this.update_shopping = this.shopping_list.filter((x) => x.list_recipe === newVal).map((x) => x.ingredient)
|
this.update_shopping = this.shopping_list.filter((x) => x.list_recipe === newVal).map((x) => x.ingredient)
|
||||||
|
this.$emit("change-servings", this.ShoppingRecipes.filter((x) => x.value === this.selected_shoppingrecipe)[0].servings)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -140,13 +140,31 @@ export default {
|
|||||||
}
|
}
|
||||||
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params).then((result) => {
|
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params).then((result) => {
|
||||||
this.shopping_list = result.data
|
this.shopping_list = result.data
|
||||||
|
|
||||||
|
if (this.add_shopping_mode) {
|
||||||
|
if (this.recipe_list) {
|
||||||
|
this.$emit(
|
||||||
|
"starting-cart",
|
||||||
|
this.shopping_list.filter((x) => x.list_recipe === this.recipe_list).map((x) => x.ingredient)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.$emit(
|
||||||
|
"starting-cart",
|
||||||
|
this.steps
|
||||||
|
.map((x) => x.ingredients)
|
||||||
|
.flat()
|
||||||
|
.filter((x) => x?.food?.food_onhand == false && x?.food?.ignore_shopping == false)
|
||||||
|
.map((x) => x.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveShopping: function (del_shopping = false) {
|
saveShopping: function (del_shopping = false) {
|
||||||
let servings = this.servings
|
let servings = this.servings
|
||||||
if (del_shopping) {
|
if (del_shopping) {
|
||||||
servings = 0
|
servings = -1
|
||||||
}
|
}
|
||||||
let params = {
|
let params = {
|
||||||
id: this.recipe,
|
id: this.recipe,
|
||||||
@ -155,7 +173,7 @@ export default {
|
|||||||
servings: servings,
|
servings: servings,
|
||||||
}
|
}
|
||||||
this.genericAPI(this.Models.RECIPE, this.Actions.SHOPPING, params)
|
this.genericAPI(this.Models.RECIPE, this.Actions.SHOPPING, params)
|
||||||
.then(() => {
|
.then((result) => {
|
||||||
if (del_shopping) {
|
if (del_shopping) {
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||||
} else if (this.selected_shoppingrecipe) {
|
} else if (this.selected_shoppingrecipe) {
|
||||||
@ -164,13 +182,6 @@ export default {
|
|||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
|
||||||
if (!this.add_shopping_mode) {
|
|
||||||
return this.getShopping(false)
|
|
||||||
} else {
|
|
||||||
this.$emit("shopping-added")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (del_shopping) {
|
if (del_shopping) {
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||||
@ -186,13 +197,51 @@ export default {
|
|||||||
// ALERT: this will all break if ingredients are re-used between recipes
|
// ALERT: this will all break if ingredients are re-used between recipes
|
||||||
if (e.add) {
|
if (e.add) {
|
||||||
this.update_shopping.push(e.item.id)
|
this.update_shopping.push(e.item.id)
|
||||||
|
this.shopping_list.push({
|
||||||
|
id: Math.random(),
|
||||||
|
amount: e.item.amount,
|
||||||
|
ingredient: e.item.id,
|
||||||
|
food: e.item.food,
|
||||||
|
list_recipe: this.selected_shoppingrecipe,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.update_shopping = this.update_shopping.filter((x) => x !== e.item.id)
|
this.update_shopping = [...this.update_shopping.filter((x) => x !== e.item.id)]
|
||||||
|
this.shopping_list = [...this.shopping_list.filter((x) => !(x.ingredient === e.item.id && x.list_recipe === this.selected_shoppingrecipe))]
|
||||||
}
|
}
|
||||||
if (this.add_shopping_mode) {
|
if (this.add_shopping_mode) {
|
||||||
this.$emit("add-to-shopping", e)
|
this.$emit("add-to-shopping", e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
prepareIngredient: function (i) {
|
||||||
|
let shopping = this.shopping_list.filter((x) => x.ingredient === i.id)
|
||||||
|
let selected_list = this.shopping_list.filter((x) => x.list_recipe === this.selected_shoppingrecipe && x.ingredient === i.id)
|
||||||
|
// checked = in the selected shopping list OR if in shoppping mode without a selected recipe, the default value true unless it is ignored or onhand
|
||||||
|
let checked = selected_list.length > 0 || (this.add_shopping_mode && !this.selected_shoppingrecipe && !i?.food?.ignore_recipe && !i?.food?.food_onhand)
|
||||||
|
|
||||||
|
let shopping_status = false // not in shopping list
|
||||||
|
if (shopping.length > 0) {
|
||||||
|
if (selected_list.length > 0) {
|
||||||
|
shopping_status = true // in shopping list for *this* recipe
|
||||||
|
} else {
|
||||||
|
shopping_status = null // in shopping list but not *this* recipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
shop: checked,
|
||||||
|
shopping_status: shopping_status, // possible values: true, false, null
|
||||||
|
category: i.food.supermarket_category?.name,
|
||||||
|
shopping_list: shopping.map((x) => {
|
||||||
|
return {
|
||||||
|
mealplan: x?.recipe_mealplan?.name,
|
||||||
|
amount: x.amount,
|
||||||
|
food: x.food?.name,
|
||||||
|
unit: x.unit?.name,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -71,9 +71,7 @@ export default {
|
|||||||
image_placeholder: window.IMAGE_PLACEHOLDER,
|
image_placeholder: window.IMAGE_PLACEHOLDER,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {},
|
||||||
console.log(this.value)
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
entry: function () {
|
entry: function () {
|
||||||
return this.value.originalItem
|
return this.value.originalItem
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
<b-form-group>
|
<b-form-group>
|
||||||
<generic-multiselect
|
<generic-multiselect
|
||||||
@change="selectRecipe"
|
@change="selectRecipe"
|
||||||
:initial_selection="entryEditing_initial_recipe"
|
:initial_single_selection="entryEditing.recipe"
|
||||||
:label="'name'"
|
:label="'name'"
|
||||||
:model="Models.RECIPE"
|
:model="Models.RECIPE"
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
@ -45,7 +45,7 @@
|
|||||||
v-bind:placeholder="$t('Meal_Type')"
|
v-bind:placeholder="$t('Meal_Type')"
|
||||||
:limit="10"
|
:limit="10"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:initial_selection="entryEditing_initial_meal_type"
|
:initial_single_selection="entryEditing.meal_type"
|
||||||
:allow_create="true"
|
:allow_create="true"
|
||||||
:create_placeholder="$t('Create_New_Meal_Type')"
|
:create_placeholder="$t('Create_New_Meal_Type')"
|
||||||
@new="createMealType"
|
@new="createMealType"
|
||||||
@ -76,13 +76,16 @@
|
|||||||
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
|
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-input-group v-if="!autoMealPlan">
|
<b-input-group v-if="!autoMealPlan">
|
||||||
<b-form-checkbox id="AddToShopping" v-model="entryEditing.addshopping" />
|
<b-form-checkbox id="AddToShopping" v-model="mealplan_settings.addshopping" />
|
||||||
<small tabindex="-1" class="form-text text-muted">{{ $t("AddToShopping") }}</small>
|
<small tabindex="-1" class="form-text text-muted">{{ $t("AddToShopping") }}</small>
|
||||||
</b-input-group>
|
</b-input-group>
|
||||||
|
<b-input-group v-if="mealplan_settings.addshopping">
|
||||||
|
<b-form-checkbox id="reviewShopping" v-model="mealplan_settings.reviewshopping" />
|
||||||
|
<small tabindex="-1" class="form-text text-muted">{{ $t("review_shopping") }}</small>
|
||||||
|
</b-input-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 d-none d-lg-block d-xl-block">
|
<div class="col-lg-6 d-none d-lg-block d-xl-block">
|
||||||
<recipe-card v-if="entryEditing.recipe && !entryEditing.addshopping" :recipe="entryEditing.recipe" :detailed="false"></recipe-card>
|
<recipe-card v-if="entryEditing.recipe" :recipe="entryEditing.recipe" :detailed="false"></recipe-card>
|
||||||
<ingredients-card v-if="entryEditing.recipe && entryEditing.addshopping" :recipe="entryEditing.recipe" :detailed="false"></ingredients-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3 mb-3">
|
<div class="row mt-3 mb-3">
|
||||||
@ -100,6 +103,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
|
import VueCookies from "vue-cookies"
|
||||||
import { BootstrapVue } from "bootstrap-vue"
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
||||||
@ -108,13 +112,13 @@ const { ApiApiFactory } = require("@/utils/openapi/api")
|
|||||||
const { StandardToasts } = require("@/utils/utils")
|
const { StandardToasts } = require("@/utils/utils")
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
Vue.use(VueCookies)
|
||||||
|
let MEALPLAN_COOKIE_NAME = "mealplan_settings"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "MealPlanEditModal",
|
name: "MealPlanEditModal",
|
||||||
props: {
|
props: {
|
||||||
entry: Object,
|
entry: Object,
|
||||||
entryEditing_initial_recipe: Array,
|
|
||||||
entryEditing_initial_meal_type: Array,
|
|
||||||
entryEditing_inital_servings: Number,
|
entryEditing_inital_servings: Number,
|
||||||
modal_title: String,
|
modal_title: String,
|
||||||
modal_id: {
|
modal_id: {
|
||||||
@ -130,7 +134,6 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
GenericMultiselect,
|
GenericMultiselect,
|
||||||
RecipeCard: () => import("@/components/RecipeCard.vue"),
|
RecipeCard: () => import("@/components/RecipeCard.vue"),
|
||||||
IngredientsCard: () => import("@/components/IngredientsCard.vue"),
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -138,18 +141,36 @@ export default {
|
|||||||
missing_recipe: false,
|
missing_recipe: false,
|
||||||
missing_meal_type: false,
|
missing_meal_type: false,
|
||||||
default_plan_share: [],
|
default_plan_share: [],
|
||||||
|
mealplan_settings: {
|
||||||
|
addshopping: false,
|
||||||
|
reviewshopping: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
entry: {
|
entry: {
|
||||||
handler() {
|
handler() {
|
||||||
this.entryEditing = Object.assign({}, this.entry)
|
this.entryEditing = Object.assign({}, this.entry)
|
||||||
|
|
||||||
if (this.entryEditing_inital_servings) {
|
if (this.entryEditing_inital_servings) {
|
||||||
this.entryEditing.servings = this.entryEditing_inital_servings
|
this.entryEditing.servings = this.entryEditing_inital_servings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
entryEditing: {
|
||||||
|
handler(newVal) {},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
mealplan_settings: {
|
||||||
|
handler(newVal) {
|
||||||
|
this.$cookies.set(MEALPLAN_COOKIE_NAME, this.mealplan_settings)
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
entryEditing_inital_servings: function (newVal) {
|
||||||
|
this.entryEditing.servings = newVal
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted: function () {},
|
mounted: function () {},
|
||||||
computed: {
|
computed: {
|
||||||
@ -159,6 +180,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showModal() {
|
showModal() {
|
||||||
|
if (this.$cookies.isKey(MEALPLAN_COOKIE_NAME)) {
|
||||||
|
this.mealplan_settings = Object.assign({}, this.mealplan_settings, this.$cookies.get(MEALPLAN_COOKIE_NAME))
|
||||||
|
}
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
apiClient.listUserPreferences().then((result) => {
|
apiClient.listUserPreferences().then((result) => {
|
||||||
@ -181,8 +205,10 @@ export default {
|
|||||||
cancel = true
|
cancel = true
|
||||||
}
|
}
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
|
console.log("saving", { ...this.mealplan_settings, ...this.entryEditing })
|
||||||
this.$bvModal.hide(`edit-modal`)
|
this.$bvModal.hide(`edit-modal`)
|
||||||
this.$emit("save-entry", this.entryEditing)
|
this.$emit("save-entry", { ...this.mealplan_settings, ...this.entryEditing })
|
||||||
|
console.log("after emit", { ...this.mealplan_settings, ...this.entryEditing }.addshopping)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteEntry() {
|
deleteEntry() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-modal :id="`shopping_${this.modal_id}`" hide-footer @show="loadRecipe">
|
<b-modal :id="`shopping_${this.modal_id}`" @show="loadRecipe">
|
||||||
<template v-slot:modal-title
|
<template v-slot:modal-title
|
||||||
><h4>{{ $t("Add_Servings_to_Shopping", { servings: recipe_servings }) }}</h4></template
|
><h4>{{ $t("Add_Servings_to_Shopping", { servings: recipe_servings }) }}</h4></template
|
||||||
>
|
>
|
||||||
@ -16,10 +16,11 @@
|
|||||||
:recipe="recipe.id"
|
:recipe="recipe.id"
|
||||||
:ingredient_factor="ingredient_factor"
|
:ingredient_factor="ingredient_factor"
|
||||||
:servings="recipe_servings"
|
:servings="recipe_servings"
|
||||||
:show_shopping="true"
|
|
||||||
:add_shopping_mode="true"
|
:add_shopping_mode="true"
|
||||||
|
:recipe_list="list_recipe"
|
||||||
:header="false"
|
:header="false"
|
||||||
@add-to-shopping="addShopping($event)"
|
@add-to-shopping="addShopping($event)"
|
||||||
|
@starting-cart="add_shopping = $event"
|
||||||
/>
|
/>
|
||||||
</b-collapse>
|
</b-collapse>
|
||||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||||
@ -34,10 +35,11 @@
|
|||||||
:recipe="r.recipe.id"
|
:recipe="r.recipe.id"
|
||||||
:ingredient_factor="ingredient_factor"
|
:ingredient_factor="ingredient_factor"
|
||||||
:servings="recipe_servings"
|
:servings="recipe_servings"
|
||||||
:show_shopping="true"
|
|
||||||
:add_shopping_mode="true"
|
:add_shopping_mode="true"
|
||||||
|
:recipe_list="list_recipe"
|
||||||
:header="false"
|
:header="false"
|
||||||
@add-to-shopping="addShopping($event)"
|
@add-to-shopping="addShopping($event)"
|
||||||
|
@starting-cart="add_shopping = [...add_shopping, ...$event]"
|
||||||
/>
|
/>
|
||||||
</b-collapse>
|
</b-collapse>
|
||||||
</b-card>
|
</b-card>
|
||||||
@ -46,18 +48,20 @@
|
|||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-input-group class="my-3">
|
<template #modal-footer="">
|
||||||
<b-input-group-prepend is-text>
|
<b-input-group class="mr-3">
|
||||||
{{ $t("Servings") }}
|
<b-input-group-prepend is-text>
|
||||||
</b-input-group-prepend>
|
{{ $t("Servings") }}
|
||||||
|
</b-input-group-prepend>
|
||||||
|
|
||||||
<b-form-spinbutton min="1" v-model="recipe_servings" inline style="height: 3em"></b-form-spinbutton>
|
<b-form-spinbutton min="1" v-model="recipe_servings" inline style="height: 3em"></b-form-spinbutton>
|
||||||
|
|
||||||
<b-input-group-append>
|
<b-input-group-append>
|
||||||
<b-button variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
|
<b-button variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
|
||||||
<b-button variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
|
<b-button variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
|
||||||
</b-input-group-append>
|
</b-input-group-append>
|
||||||
</b-input-group>
|
</b-input-group>
|
||||||
|
</template>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -80,6 +84,8 @@ export default {
|
|||||||
recipe: { required: true, type: Object },
|
recipe: { required: true, type: Object },
|
||||||
servings: { type: Number, default: undefined },
|
servings: { type: Number, default: undefined },
|
||||||
modal_id: { required: true, type: Number },
|
modal_id: { required: true, type: Number },
|
||||||
|
mealplan: { type: Number, default: undefined },
|
||||||
|
list_recipe: { type: Number, default: undefined },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -106,7 +112,6 @@ export default {
|
|||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
servings: function (newVal) {
|
servings: function (newVal) {
|
||||||
console.log(newVal)
|
|
||||||
this.recipe_servings = parseInt(newVal)
|
this.recipe_servings = parseInt(newVal)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -121,14 +126,6 @@ export default {
|
|||||||
this.steps = result.data.steps
|
this.steps = result.data.steps
|
||||||
// ALERT: this will all break if ingredients are re-used between recipes
|
// ALERT: this will all break if ingredients are re-used between recipes
|
||||||
// ALERT: this also doesn't quite work right if the same recipe appears multiple time in the related recipes
|
// ALERT: this also doesn't quite work right if the same recipe appears multiple time in the related recipes
|
||||||
this.add_shopping = [
|
|
||||||
...this.add_shopping,
|
|
||||||
...this.steps
|
|
||||||
.map((x) => x.ingredients)
|
|
||||||
.flat()
|
|
||||||
.filter((x) => !x?.food?.food_onhand)
|
|
||||||
.map((x) => x.id),
|
|
||||||
]
|
|
||||||
if (!this.recipe_servings) {
|
if (!this.recipe_servings) {
|
||||||
this.recipe_servings = result.data?.servings
|
this.recipe_servings = result.data?.servings
|
||||||
}
|
}
|
||||||
@ -155,18 +152,20 @@ export default {
|
|||||||
})
|
})
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
})
|
})
|
||||||
.then(() => {
|
// .then(() => {
|
||||||
this.add_shopping = [
|
// if (!this.list_recipe) {
|
||||||
...this.add_shopping,
|
// this.add_shopping = [
|
||||||
...this.related_recipes
|
// ...this.add_shopping,
|
||||||
.map((x) => x.steps)
|
// ...this.related_recipes
|
||||||
.flat()
|
// .map((x) => x.steps)
|
||||||
.map((x) => x.ingredients)
|
// .flat()
|
||||||
.flat()
|
// .map((x) => x.ingredients)
|
||||||
.filter((x) => !x.food.override_ignore)
|
// .flat()
|
||||||
.map((x) => x.id),
|
// .filter((x) => !x.food.override_ignore)
|
||||||
]
|
// .map((x) => x.id),
|
||||||
})
|
// ]
|
||||||
|
// }
|
||||||
|
// })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addShopping: function (e) {
|
addShopping: function (e) {
|
||||||
@ -182,6 +181,8 @@ export default {
|
|||||||
id: this.recipe.id,
|
id: this.recipe.id,
|
||||||
ingredients: this.add_shopping,
|
ingredients: this.add_shopping,
|
||||||
servings: this.recipe_servings,
|
servings: this.recipe_servings,
|
||||||
|
mealplan: this.mealplan,
|
||||||
|
list_recipe: this.list_recipe,
|
||||||
}
|
}
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
apiClient
|
apiClient
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
<cook-log :recipe="recipe" :modal_id="modal_id"></cook-log>
|
<cook-log :recipe="recipe" :modal_id="modal_id"></cook-log>
|
||||||
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id" :entryEditing_inital_servings="servings_value"></add-recipe-to-book>
|
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id" :entryEditing_inital_servings="servings_value"></add-recipe-to-book>
|
||||||
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" />
|
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" :mealplan="undefined" />
|
||||||
|
|
||||||
<b-modal :id="`modal-share-link_${modal_id}`" v-bind:title="$t('Share')" hide-footer>
|
<b-modal :id="`modal-share-link_${modal_id}`" v-bind:title="$t('Share')" hide-footer>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -65,9 +65,7 @@
|
|||||||
|
|
||||||
<meal-plan-edit-modal
|
<meal-plan-edit-modal
|
||||||
:entry="entryEditing"
|
:entry="entryEditing"
|
||||||
:entryEditing_initial_recipe="[recipe]"
|
|
||||||
:entryEditing_inital_servings="servings_value"
|
:entryEditing_inital_servings="servings_value"
|
||||||
:entry-editing_initial_meal_type="[]"
|
|
||||||
@save-entry="saveMealPlan"
|
@save-entry="saveMealPlan"
|
||||||
:modal_id="`modal-meal-plan_${modal_id}`"
|
:modal_id="`modal-meal-plan_${modal_id}`"
|
||||||
:allow_delete="false"
|
:allow_delete="false"
|
||||||
@ -118,6 +116,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
entryEditing: {},
|
entryEditing: {},
|
||||||
|
mealplan: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@ -147,12 +146,19 @@ export default {
|
|||||||
},
|
},
|
||||||
saveMealPlan: function (entry) {
|
saveMealPlan: function (entry) {
|
||||||
entry.date = moment(entry.date).format("YYYY-MM-DD")
|
entry.date = moment(entry.date).format("YYYY-MM-DD")
|
||||||
|
let reviewshopping = entry.addshopping && entry.reviewshopping
|
||||||
|
entry.addshopping = entry.addshopping && !entry.reviewshopping
|
||||||
|
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
apiClient
|
apiClient
|
||||||
.createMealPlan(entry)
|
.createMealPlan(entry)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.$bvModal.hide(`modal-meal-plan_${this.modal_id}`)
|
this.$bvModal.hide(`modal-meal-plan_${this.modal_id}`)
|
||||||
|
if (reviewshopping) {
|
||||||
|
this.mealplan = result.data.id
|
||||||
|
this.servings_value = result.data.servings
|
||||||
|
this.addToShopping()
|
||||||
|
}
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -163,7 +169,9 @@ export default {
|
|||||||
this.entryEditing = this.options.entryEditing
|
this.entryEditing = this.options.entryEditing
|
||||||
this.entryEditing.recipe = this.recipe
|
this.entryEditing.recipe = this.recipe
|
||||||
this.entryEditing.date = moment(new Date()).format("YYYY-MM-DD")
|
this.entryEditing.date = moment(new Date()).format("YYYY-MM-DD")
|
||||||
this.$bvModal.show(`modal-meal-plan_${this.modal_id}`)
|
this.$nextTick(function () {
|
||||||
|
this.$bvModal.show(`modal-meal-plan_${this.modal_id}`)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
createShareLink: function () {
|
createShareLink: function () {
|
||||||
axios
|
axios
|
||||||
|
@ -1,334 +1,311 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="shopping_line_item">
|
<div id="shopping_line_item">
|
||||||
<b-row align-h="start">
|
|
||||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
|
|
||||||
v-if="settings.left_handed">
|
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked"
|
|
||||||
@change="updateChecked"
|
|
||||||
:key="entries[0].id"/>
|
|
||||||
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
|
||||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
|
|
||||||
</div>
|
|
||||||
</b-button>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="1" class="align-items-center d-flex">
|
|
||||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
|
||||||
@click.stop="$emit('open-context-menu', $event, entries)">
|
|
||||||
<button
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
type="button"
|
|
||||||
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
|
||||||
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret">
|
|
||||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
|
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked"
|
|
||||||
@change="updateChecked"
|
|
||||||
:key="entries[0].id"/>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="8" md="9">
|
|
||||||
<b-row class="d-flex h-100">
|
|
||||||
<b-col cols="5" md="3" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
|
|
||||||
<strong class="mr-1">{{ Object.entries(formatAmount)[0][1] }}</strong> {{
|
|
||||||
Object.entries(formatAmount)[0][0]
|
|
||||||
}}
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="5" md="3" class="d-flex flex-column" v-if="Object.entries(formatAmount).length != 1">
|
|
||||||
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }}  
|
|
||||||
{{ x[0] }}
|
|
||||||
</div>
|
|
||||||
</b-col>
|
|
||||||
|
|
||||||
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
|
||||||
{{ formatFood }}
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3" data-html2canvas-ignore="true"
|
|
||||||
class="align-items-center d-none d-md-flex justify-content-end">
|
|
||||||
<b-button size="sm" @click="showDetails = !showDetails" class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none"
|
|
||||||
variant="link">
|
|
||||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate"
|
|
||||||
:class="showDetails ? 'rotated' : ''"></i> <span
|
|
||||||
class="d-none d-md-inline-block">{{ $t('Details') }}</span>
|
|
||||||
</div>
|
|
||||||
</b-button>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
|
|
||||||
v-if="!settings.left_handed">
|
|
||||||
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
|
||||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
|
|
||||||
</div>
|
|
||||||
</b-button>
|
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked"
|
|
||||||
@change="updateChecked"
|
|
||||||
:key="entries[0].id"/>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row align-h="center" class="d-none d-md-flex">
|
|
||||||
<b-col cols="12">
|
|
||||||
<div class="small text-muted text-truncate">{{ formatHint }}</div>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<!-- detail rows -->
|
|
||||||
<div class="card no-body mb-1 pt-2 align-content-center shadow-sm" v-if="showDetails">
|
|
||||||
<div v-for="(e, x) in entries" :key="e.id">
|
|
||||||
<b-row class="small justify-content-around">
|
|
||||||
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
|
|
||||||
<button
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-link btn-sm m-0 p-0 pl-2"
|
|
||||||
style="text-overflow: ellipsis"
|
|
||||||
@click.stop="openRecipeCard($event, e)"
|
|
||||||
@mouseover="openRecipeCard($event, e)">
|
|
||||||
{{ formatOneRecipe(e) }}
|
|
||||||
</button>
|
|
||||||
</b-col>
|
|
||||||
<b-col cols="auto" md="4" class="text-muted">{{ formatOneMealPlan(e) }}</b-col>
|
|
||||||
<b-col cols="auto" md="4" class="text-muted text-right overflow-hidden text-nowrap pr-4">
|
|
||||||
{{ formatOneCreatedBy(e) }}
|
|
||||||
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
|
|
||||||
</b-col>
|
|
||||||
</b-row>
|
|
||||||
<b-row align-h="start">
|
<b-row align-h="start">
|
||||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0" v-if="settings.left_handed">
|
||||||
v-if="settings.left_handed">
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
||||||
:checked="formatChecked"
|
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i></div>
|
||||||
@change="updateChecked"
|
</b-button>
|
||||||
:key="entries[0].id"/>
|
</b-col>
|
||||||
</b-col>
|
<b-col cols="1" class="align-items-center d-flex">
|
||||||
<b-col cols="1" class="align-items-center d-flex">
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" @click.stop="$emit('open-context-menu', $event, entries)">
|
||||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
<button
|
||||||
@click.stop="$emit('open-context-menu', $event, e)">
|
aria-haspopup="true"
|
||||||
<button
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
type="button"
|
||||||
aria-expanded="false"
|
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
||||||
type="button"
|
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
|
||||||
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
>
|
||||||
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret">
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</b-col>
|
||||||
</b-col>
|
<b-col cols="1" class="px-1 justify-content-center align-items-center d-none d-md-flex">
|
||||||
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
|
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked"
|
</b-col>
|
||||||
@change="updateChecked"
|
<b-col cols="8" md="9">
|
||||||
:key="entries[0].id"/>
|
<b-row class="d-flex h-100">
|
||||||
</b-col>
|
<b-col cols="5" md="3" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
|
||||||
<b-col cols="8" md="9">
|
<strong class="mr-1">{{ Object.entries(formatAmount)[0][1] }}</strong> {{ Object.entries(formatAmount)[0][0] }}
|
||||||
<b-row class="d-flex align-items-center h-100">
|
</b-col>
|
||||||
<b-col cols="5" md="3" class="d-flex align-items-center">
|
<b-col cols="5" md="3" class="d-flex flex-column" v-if="Object.entries(formatAmount).length != 1">
|
||||||
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}
|
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">
|
||||||
</b-col>
|
{{ x[1] }}  
|
||||||
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
{{ x[0] }}
|
||||||
{{ formatOneFood(e) }}
|
</div>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" class="d-flex d-md-none">
|
|
||||||
<div class="small text-muted text-truncate" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
|
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
||||||
</b-col>
|
{{ formatFood }}
|
||||||
</b-row>
|
</b-col>
|
||||||
</b-col>
|
<b-col cols="3" data-html2canvas-ignore="true" class="align-items-center d-none d-md-flex justify-content-end">
|
||||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
|
<b-button size="sm" @click="showDetails = !showDetails" class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none" variant="link">
|
||||||
v-if="!settings.left_handed">
|
<div class="text-nowrap">
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
<i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i> <span class="d-none d-md-inline-block">{{ $t("Details") }}</span>
|
||||||
:checked="formatChecked"
|
</div>
|
||||||
@change="updateChecked"
|
</b-button>
|
||||||
:key="entries[0].id"/>
|
</b-col>
|
||||||
</b-col>
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none" v-if="!settings.left_handed">
|
||||||
|
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
||||||
|
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i></div>
|
||||||
|
</b-button>
|
||||||
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
<hr class="w-75" v-if="x !== entries.length -1"/>
|
<b-row align-h="center" class="d-none d-md-flex">
|
||||||
<div class="pb-4" v-if="x === entries.length -1"></div>
|
<b-col cols="12">
|
||||||
</div>
|
<div class="small text-muted text-truncate">{{ formatHint }}</div>
|
||||||
</div>
|
</b-col>
|
||||||
<hr class="m-1" v-if="!showDetails"/>
|
</b-row>
|
||||||
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
|
<!-- detail rows -->
|
||||||
<template #menu="{ contextData }" v-if="recipe">
|
<div class="card no-body mb-1 pt-2 align-content-center shadow-sm" v-if="showDetails">
|
||||||
<ContextMenuItem>
|
<div v-for="(e, x) in entries" :key="e.id">
|
||||||
<RecipeCard :recipe="contextData" :detail="false"></RecipeCard>
|
<b-row class="small justify-content-around">
|
||||||
</ContextMenuItem>
|
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
|
||||||
<ContextMenuItem @click="$refs.menu.close()">
|
<button
|
||||||
<b-form-group label-cols="9" content-cols="3" class="text-nowrap m-0 mr-2">
|
aria-haspopup="true"
|
||||||
<template #label>
|
aria-expanded="false"
|
||||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-pizza-slice"></i> {{ $t("Servings") }}</a>
|
type="button"
|
||||||
</template>
|
class="btn btn-link btn-sm m-0 p-0 pl-2"
|
||||||
<div @click.prevent.stop>
|
style="text-overflow: ellipsis"
|
||||||
<b-form-input class="mt-2" min="0" type="number" v-model="servings"></b-form-input>
|
@click.stop="openRecipeCard($event, e)"
|
||||||
|
@mouseover="openRecipeCard($event, e)"
|
||||||
|
>
|
||||||
|
{{ formatOneRecipe(e) }}
|
||||||
|
</button>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="auto" md="4" class="text-muted">{{ formatOneMealPlan(e) }}</b-col>
|
||||||
|
<b-col cols="auto" md="4" class="text-muted text-right overflow-hidden text-nowrap pr-4">
|
||||||
|
{{ formatOneCreatedBy(e) }}
|
||||||
|
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row align-h="start">
|
||||||
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0" v-if="settings.left_handed">
|
||||||
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="align-items-center d-flex">
|
||||||
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" @click.stop="$emit('open-context-menu', $event, e)">
|
||||||
|
<button
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
type="button"
|
||||||
|
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
||||||
|
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
|
||||||
|
>
|
||||||
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
|
||||||
|
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="8" md="9">
|
||||||
|
<b-row class="d-flex align-items-center h-100">
|
||||||
|
<b-col cols="5" md="3" class="d-flex align-items-center">
|
||||||
|
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
||||||
|
{{ formatOneFood(e) }}
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="12" class="d-flex d-md-none">
|
||||||
|
<div class="small text-muted text-truncate" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none" v-if="!settings.left_handed">
|
||||||
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<hr class="w-75" v-if="x !== entries.length - 1" />
|
||||||
|
<div class="pb-4" v-if="x === entries.length - 1"></div>
|
||||||
</div>
|
</div>
|
||||||
</b-form-group>
|
</div>
|
||||||
</ContextMenuItem>
|
<hr class="m-1" v-if="!showDetails" />
|
||||||
</template>
|
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
|
||||||
</ContextMenu>
|
<template #menu="{ contextData }" v-if="recipe">
|
||||||
</div>
|
<ContextMenuItem>
|
||||||
|
<RecipeCard :recipe="contextData" :detail="false"></RecipeCard>
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem @click="$refs.menu.close()">
|
||||||
|
<b-form-group label-cols="9" content-cols="3" class="text-nowrap m-0 mr-2">
|
||||||
|
<template #label>
|
||||||
|
<a class="dropdown-item p-2" href="#"><i class="fas fa-pizza-slice"></i> {{ $t("Servings") }}</a>
|
||||||
|
</template>
|
||||||
|
<div @click.prevent.stop>
|
||||||
|
<b-form-input class="mt-2" min="0" type="number" v-model="servings"></b-form-input>
|
||||||
|
</div>
|
||||||
|
</b-form-group>
|
||||||
|
</ContextMenuItem>
|
||||||
|
</template>
|
||||||
|
</ContextMenu>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
import {BootstrapVue} from "bootstrap-vue"
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||||
import {ApiMixin} from "@/utils/utils"
|
import { ApiMixin } from "@/utils/utils"
|
||||||
import RecipeCard from "./RecipeCard.vue"
|
import RecipeCard from "./RecipeCard.vue"
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||||
// or i'm capturing it incorrectly
|
// or i'm capturing it incorrectly
|
||||||
name: "ShoppingLineItem",
|
name: "ShoppingLineItem",
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
components: {RecipeCard, ContextMenu, ContextMenuItem},
|
components: { RecipeCard, ContextMenu, ContextMenuItem },
|
||||||
props: {
|
props: {
|
||||||
entries: {
|
entries: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
},
|
||||||
|
settings: Object,
|
||||||
|
groupby: { type: String },
|
||||||
},
|
},
|
||||||
settings: Object,
|
data() {
|
||||||
groupby: {type: String},
|
return {
|
||||||
},
|
showDetails: false,
|
||||||
data() {
|
recipe: undefined,
|
||||||
return {
|
servings: 1,
|
||||||
showDetails: false,
|
|
||||||
recipe: undefined,
|
|
||||||
servings: 1,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
formatAmount: function () {
|
|
||||||
let amount = {}
|
|
||||||
this.entries.forEach((entry) => {
|
|
||||||
let unit = entry?.unit?.name ?? "----"
|
|
||||||
if (entry.amount) {
|
|
||||||
if (amount[unit]) {
|
|
||||||
amount[unit] += entry.amount
|
|
||||||
} else {
|
|
||||||
amount[unit] = entry.amount
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
for (const [k, v] of Object.entries(amount)) {
|
|
||||||
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places
|
|
||||||
}
|
|
||||||
return amount
|
|
||||||
},
|
},
|
||||||
formatCategory: function () {
|
computed: {
|
||||||
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
formatAmount: function () {
|
||||||
},
|
let amount = {}
|
||||||
formatChecked: function () {
|
this.entries.forEach((entry) => {
|
||||||
return this.entries.map((x) => x.checked).every((x) => x === true)
|
let unit = entry?.unit?.name ?? "----"
|
||||||
},
|
if (entry.amount) {
|
||||||
formatHint: function () {
|
if (amount[unit]) {
|
||||||
if (this.groupby == "recipe") {
|
amount[unit] += entry.amount
|
||||||
return this.formatCategory
|
} else {
|
||||||
} else {
|
amount[unit] = entry.amount
|
||||||
return this.formatRecipe
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
formatFood: function () {
|
|
||||||
return this.formatOneFood(this.entries[0])
|
|
||||||
},
|
|
||||||
formatUnit: function () {
|
|
||||||
return this.formatOneUnit(this.entries[0])
|
|
||||||
},
|
|
||||||
formatRecipe: function () {
|
|
||||||
if (this.entries?.length == 1) {
|
|
||||||
return this.formatOneMealPlan(this.entries[0]) || ""
|
|
||||||
} else {
|
|
||||||
let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
|
|
||||||
// return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
|
|
||||||
|
|
||||||
return mealplan_name
|
|
||||||
.map((x) => {
|
|
||||||
return this.formatOneMealPlan(x)
|
|
||||||
})
|
})
|
||||||
.join(" - ")
|
for (const [k, v] of Object.entries(amount)) {
|
||||||
}
|
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places
|
||||||
},
|
}
|
||||||
formatNotes: function () {
|
return amount
|
||||||
if (this.entries?.length == 1) {
|
},
|
||||||
return this.formatOneNote(this.entries[0]) || ""
|
formatCategory: function () {
|
||||||
}
|
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
||||||
return ""
|
},
|
||||||
},
|
formatChecked: function () {
|
||||||
},
|
return this.entries.map((x) => x.checked).every((x) => x === true)
|
||||||
watch: {},
|
},
|
||||||
mounted() {
|
formatHint: function () {
|
||||||
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
|
if (this.groupby == "recipe") {
|
||||||
},
|
return this.formatCategory
|
||||||
methods: {
|
} else {
|
||||||
// this.genericAPI inherited from ApiMixin
|
return this.formatRecipe
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatFood: function () {
|
||||||
|
return this.formatOneFood(this.entries[0])
|
||||||
|
},
|
||||||
|
formatUnit: function () {
|
||||||
|
return this.formatOneUnit(this.entries[0])
|
||||||
|
},
|
||||||
|
formatRecipe: function () {
|
||||||
|
if (this.entries?.length == 1) {
|
||||||
|
return this.formatOneMealPlan(this.entries[0]) || ""
|
||||||
|
} else {
|
||||||
|
let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
|
||||||
|
// return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
|
||||||
|
|
||||||
formatDate: function (datetime) {
|
return mealplan_name
|
||||||
if (!datetime) {
|
.map((x) => {
|
||||||
return
|
return this.formatOneMealPlan(x)
|
||||||
}
|
})
|
||||||
return Intl.DateTimeFormat(window.navigator.language, {
|
.join(" - ")
|
||||||
dateStyle: "short",
|
}
|
||||||
timeStyle: "short"
|
},
|
||||||
}).format(Date.parse(datetime))
|
formatNotes: function () {
|
||||||
|
if (this.entries?.length == 1) {
|
||||||
|
return this.formatOneNote(this.entries[0]) || ""
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
},
|
},
|
||||||
formatOneAmount: function (item) {
|
watch: {},
|
||||||
return item?.amount ?? 1
|
mounted() {
|
||||||
|
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
|
||||||
},
|
},
|
||||||
formatOneUnit: function (item) {
|
methods: {
|
||||||
return item?.unit?.name ?? ""
|
// this.genericAPI inherited from ApiMixin
|
||||||
|
|
||||||
|
formatDate: function (datetime) {
|
||||||
|
if (!datetime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return Intl.DateTimeFormat(window.navigator.language, {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "short",
|
||||||
|
}).format(Date.parse(datetime))
|
||||||
|
},
|
||||||
|
formatOneAmount: function (item) {
|
||||||
|
return item?.amount ?? 1
|
||||||
|
},
|
||||||
|
formatOneUnit: function (item) {
|
||||||
|
return item?.unit?.name ?? ""
|
||||||
|
},
|
||||||
|
formatOneCategory: function (item) {
|
||||||
|
return item?.food?.supermarket_category?.name
|
||||||
|
},
|
||||||
|
formatOneCompletedAt: function (item) {
|
||||||
|
if (!item.completed_at) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
|
||||||
|
},
|
||||||
|
formatOneFood: function (item) {
|
||||||
|
return item.food.name
|
||||||
|
},
|
||||||
|
formatOneDelayUntil: function (item) {
|
||||||
|
if (!item.delay_until || (item.delay_until && item.checked)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
|
||||||
|
},
|
||||||
|
formatOneMealPlan: function (item) {
|
||||||
|
return item?.recipe_mealplan?.name ?? ""
|
||||||
|
},
|
||||||
|
formatOneRecipe: function (item) {
|
||||||
|
return item?.recipe_mealplan?.recipe_name ?? ""
|
||||||
|
},
|
||||||
|
formatOneNote: function (item) {
|
||||||
|
if (!item) {
|
||||||
|
item = this.entries[0]
|
||||||
|
}
|
||||||
|
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
|
||||||
|
},
|
||||||
|
formatOneCreatedBy: function (item) {
|
||||||
|
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
|
||||||
|
},
|
||||||
|
openRecipeCard: function (e, item) {
|
||||||
|
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, { id: item.recipe_mealplan.recipe }).then((result) => {
|
||||||
|
let recipe = result.data
|
||||||
|
recipe.steps = undefined
|
||||||
|
this.recipe = true
|
||||||
|
this.$refs.recipe_card.open(e, recipe)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateChecked: function (e, item) {
|
||||||
|
let update = undefined
|
||||||
|
if (!item) {
|
||||||
|
update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
|
||||||
|
} else {
|
||||||
|
update = { entries: [item], checked: !item.checked }
|
||||||
|
}
|
||||||
|
this.$emit("update-checkbox", update)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
formatOneCategory: function (item) {
|
|
||||||
return item?.food?.supermarket_category?.name
|
|
||||||
},
|
|
||||||
formatOneCompletedAt: function (item) {
|
|
||||||
if (!item.completed_at) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
|
|
||||||
},
|
|
||||||
formatOneFood: function (item) {
|
|
||||||
return item.food.name
|
|
||||||
},
|
|
||||||
formatOneDelayUntil: function (item) {
|
|
||||||
if (!item.delay_until || (item.delay_until && item.checked)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
|
|
||||||
},
|
|
||||||
formatOneMealPlan: function (item) {
|
|
||||||
return item?.recipe_mealplan?.name ?? ""
|
|
||||||
},
|
|
||||||
formatOneRecipe: function (item) {
|
|
||||||
return item?.recipe_mealplan?.recipe_name ?? ""
|
|
||||||
},
|
|
||||||
formatOneNote: function (item) {
|
|
||||||
if (!item) {
|
|
||||||
item = this.entries[0]
|
|
||||||
}
|
|
||||||
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
|
|
||||||
},
|
|
||||||
formatOneCreatedBy: function (item) {
|
|
||||||
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
|
|
||||||
},
|
|
||||||
openRecipeCard: function (e, item) {
|
|
||||||
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, {id: item.recipe_mealplan.recipe}).then((result) => {
|
|
||||||
let recipe = result.data
|
|
||||||
recipe.steps = undefined
|
|
||||||
this.recipe = true
|
|
||||||
this.$refs.recipe_card.open(e, recipe)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateChecked: function (e, item) {
|
|
||||||
let update = undefined
|
|
||||||
if (!item) {
|
|
||||||
update = {entries: this.entries.map((x) => x.id), checked: !this.formatChecked}
|
|
||||||
} else {
|
|
||||||
update = {entries: [item], checked: !item.checked}
|
|
||||||
}
|
|
||||||
this.$emit("update-checkbox", update)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -344,34 +321,34 @@ export default {
|
|||||||
/* border-bottom: 1px solid #000; /* …and with a border on the top */
|
/* border-bottom: 1px solid #000; /* …and with a border on the top */
|
||||||
/* } */
|
/* } */
|
||||||
.checkbox-control {
|
.checkbox-control {
|
||||||
font-size: 0.6rem
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-control-mobile {
|
.checkbox-control-mobile {
|
||||||
font-size: 1rem
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotate {
|
.rotate {
|
||||||
-moz-transition: all 0.25s linear;
|
-moz-transition: all 0.25s linear;
|
||||||
-webkit-transition: all 0.25s linear;
|
-webkit-transition: all 0.25s linear;
|
||||||
transition: all 0.25s linear;
|
transition: all 0.25s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotated {
|
.rotated {
|
||||||
-moz-transform: rotate(90deg);
|
-moz-transform: rotate(90deg);
|
||||||
-webkit-transform: rotate(90deg);
|
-webkit-transform: rotate(90deg);
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit-badge-lg {
|
.unit-badge-lg {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dropdown-spacing {
|
.dropdown-spacing {
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -294,5 +294,7 @@
|
|||||||
"ignore_shopping_help": "Never add food to the shopping list (e.g. water)",
|
"ignore_shopping_help": "Never add food to the shopping list (e.g. water)",
|
||||||
"shopping_category_help": "Supermarkets can be ordered and filtered by Shopping Category according to the layout of the aisles.",
|
"shopping_category_help": "Supermarkets can be ordered and filtered by Shopping Category according to the layout of the aisles.",
|
||||||
"food_recipe_help": "Linking a recipe here will include the linked recipe in any other recipe that use this food",
|
"food_recipe_help": "Linking a recipe here will include the linked recipe in any other recipe that use this food",
|
||||||
"Foods":"Foods"
|
"Foods": "Foods",
|
||||||
|
"review_shopping": "Review shopping entries before saving",
|
||||||
|
"view_recipe": "View Recipe"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user