Merge pull request #1447 from smilerz/mealplan_shopping_useability

Mealplan shopping useability
This commit is contained in:
vabene1111 2022-02-01 08:28:59 +01:00 committed by GitHub
commit b2a415b333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 990 additions and 744 deletions

View File

@ -205,9 +205,9 @@ class CustomIsShared(permissions.BasePermission):
return request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# temporary hack to make old shopping list work with new shopping list
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())
# # temporary hack to make old shopping list work with new shopping list
# 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)

View File

@ -8,7 +8,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.models import (Ingredient, ShoppingListEntry, ShoppingListRecipe,
from cookbook.models import (Ingredient, MealPlan, Recipe, ShoppingListEntry, ShoppingListRecipe,
SupermarketCategoryRelation)
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')
# 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"))
class RecipeShoppingEditor():
def __init__(self, user, space, **kwargs):
self.created_by = user
self.space = space
self._kwargs = {**kwargs}
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"))
self.mealplan = self._kwargs.get('mealplan', None)
if type(self.mealplan) in [int, float]:
self.mealplan = MealPlan.objects.filter(id=self.mealplan, space=self.space)
self.id = self._kwargs.get('id', None)
self._shopping_list_recipe = self.get_shopping_list_recipe(self.id, self.created_by, self.space)
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)
self.recipe = getattr(self._shopping_list_recipe, 'recipe', None) or self._kwargs.get('recipe', None) or getattr(self.mealplan, 'recipe', None)
if type(self.recipe) in [int, float]:
self.recipe = Recipe.objects.filter(id=self.recipe, space=self.space)
try:
servings = float(servings)
self.servings = float(self._kwargs.get('servings', None))
except (ValueError, TypeError):
servings = getattr(mealplan, 'servings', 1.0)
self.servings = getattr(self._shopping_list_recipe, 'servings', None) or getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', None)
servings_factor = servings / r.servings
@property
def _servings_factor(self):
return self.servings / self.recipe.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
@property
def _shared_users(self):
return [*list(self.created_by.get_shopping_share()), self.created_by]
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)
@staticmethod
def get_shopping_list_recipe(id, user, space):
return ShoppingListRecipe.objects.filter(id=id).filter(Q(shoppinglist__space=space) | Q(entries__space=space)).filter(
Q(shoppinglist__created_by=user)
| Q(shoppinglist__shared=user)
| Q(entries__created_by=user)
| Q(entries__created_by__in=list(user.get_shopping_share()))
).prefetch_related('entries').first()
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
def get_recipe_ingredients(self, id, exclude_onhand=False):
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)
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])
else:
related_step_ing += Ingredient.objects.filter(step__recipe=x, space=space).values_list('id', flat=True)
return Ingredient.objects.filter(step__recipe__id=id, food__ignore_shopping=False, space=self.space)
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])
@property
def _include_related(self):
return self.created_by.userpreference.mealplan_autoinclude_related
@property
def _exclude_onhand(self):
return self.created_by.userpreference.mealplan_autoexclude_onhand
def create(self, **kwargs):
ingredients = kwargs.get('ingredients', None)
exclude_onhand = not ingredients and self._exclude_onhand
if servings := kwargs.get('servings', None):
self.servings = float(servings)
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:
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)
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))
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)
return True
# if servings have changed, update the ShoppingListRecipe and existing Entries
if servings <= 0:
servings = 1
def add(self, **kwargs):
return
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)
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()
self._shopping_list_recipe.servings = self.servings
self._shopping_list_recipe.save()
return True
def delete(self, **kwargs):
try:
self._shopping_list_recipe.delete()
return True
except:
return False
def _add_ingredients(self, ingredients=None):
if not ingredients:
return
elif type(ingredients) == list:
ingredients = Ingredient.objects.filter(id__in=ingredients)
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing)
# add any missing Entries
for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create(
list_recipe=list_recipe,
list_recipe=self._shopping_list_recipe,
food=i.food,
unit=i.unit,
ingredient=i,
amount=i.amount * Decimal(servings_factor),
created_by=created_by,
space=space,
amount=i.amount * Decimal(self._servings_factor),
created_by=self.created_by,
space=self.space,
)
# return all shopping list items
return list_recipe
# 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

View 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
),
]

View File

@ -852,11 +852,12 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
def __str__(self):
return f'Shopping list entry {self.id}'
# TODO deprecate
def get_shared(self):
try:
return self.shoppinglist_set.first().shared.all()
except AttributeError:
return self.created_by.userpreference.shopping_share.all()
# TODO deprecate
def get_owner(self):
try:
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):
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):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)

View File

@ -13,7 +13,7 @@ from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.fields import empty
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,
FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType,
NutritionInformation, Recipe, RecipeBook, RecipeBookEntry,
@ -660,7 +660,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
validated_data['created_by'] = self.context['request'].user
mealplan = super().create(validated_data)
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
class Meta:
@ -694,12 +695,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
# TODO remove once old shopping list
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
list_from_recipe(
list_recipe=instance,
servings=validated_data['servings'],
created_by=self.context['request'].user,
space=self.context['request'].space
)
SLR = RecipeShoppingEditor(user=self.context['request'].user, space=self.context['request'].space)
SLR.edit_servings(servings=validated_data['servings'], id=instance.id)
return super().update(instance, validated_data)
class Meta:

View File

@ -6,8 +6,9 @@ from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
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.models import (Food, FoodInheritField, Ingredient, MealPlan, Recipe,
ShoppingListEntry, Step)
@ -104,20 +105,31 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
@receiver(post_save, sender=MealPlan)
def auto_add_shopping(sender, instance=None, created=False, weak=False, **kwargs):
if not instance:
return
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
if not created and instance.shoppinglistrecipe_set.exists():
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 created:
# if creating a mealplan - perform shopping list activities
kwargs = {
'mealplan': instance,
'space': instance.space,
'created_by': user,
'servings': instance.servings
}
list_recipe = list_from_recipe(**kwargs)
# kwargs = {
# 'mealplan': instance,
# 'space': instance.space,
# 'created_by': user,
# 'servings': instance.servings
# }
SLR = RecipeShoppingEditor(user=user, space=instance.space)
SLR.create(mealplan=instance, servings=instance.servings)
# list_recipe = list_from_recipe(**kwargs)

View File

@ -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
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):
# check 1 entry

View File

@ -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(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}),
{'list_recipe': list_recipe, 'ingredients': keep_ing},
content_type='application/json'

View File

@ -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_search import RecipeFacet, RecipeSearch, old_search
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,
ImportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook,
RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry,
@ -153,11 +153,15 @@ class FuzzyFilterMixin(ViewSetMixin, ExtendedRecipeMixin):
)
else:
# 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
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
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)
@ -644,7 +648,6 @@ class RecipeViewSet(viewsets.ModelViewSet):
schema = QueryParamAutoSchema()
def get_queryset(self):
if self.detail:
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
@ -717,16 +720,27 @@ class RecipeViewSet(viewsets.ModelViewSet):
obj = self.get_object()
ingredients = request.data.get('ingredients', None)
servings = request.data.get('servings', None)
list_recipe = ShoppingListRecipe.objects.filter(id=request.data.get('list_recipe', None)).first()
if servings is None:
servings = getattr(list_recipe, 'servings', obj.servings)
# 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)
list_recipe = request.data.get('list_recipe', None)
mealplan = request.data.get('mealplan', None)
SLR = RecipeShoppingEditor(request.user, request.space, id=list_recipe, recipe=obj, mealplan=mealplan)
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(
detail=True,

View File

@ -54,20 +54,14 @@
<div class="col-12 col-md-3 calender-options">
<h5>{{ $t("Planner_Settings") }}</h5>
<b-form>
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')"
label-for="UomInput">
<b-form-select id="UomInput" v-model="settings.displayPeriodUom"
:options="options.displayPeriodUom"></b-form-select>
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')" label-for="UomInput">
<b-form-select id="UomInput" v-model="settings.displayPeriodUom" :options="options.displayPeriodUom"></b-form-select>
</b-form-group>
<b-form-group id="PeriodInput" :label="$t('Periods')"
: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-group id="PeriodInput" :label="$t('Periods')" :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-group>
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')"
label-for="DaysInput">
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek"
:options="dayNames"></b-form-select>
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')" label-for="DaysInput">
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek" :options="dayNames"></b-form-select>
</b-form-group>
<b-form-group id="WeekNumInput" :label="$t('Week_Numbers')">
<b-form-checkbox v-model="settings.displayWeekNumbers" name="week_num">
@ -80,22 +74,17 @@
<h5>{{ $t("Meal_Types") }}</h5>
<div>
<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
:key="meal_type.id">
<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">
<b-card-header class="p-2 border-0">
<div class="row">
<div class="col-2">
<button type="button" class="btn btn-lg shadow-none"><i
class="fas fa-arrows-alt-v"></i></button>
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
</div>
<div class="col-10">
<h5 class="mt-1 mb-1">
{{ meal_type.icon }} {{
meal_type.name
{{ meal_type.icon }} {{ meal_type.name
}}<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" v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }" @click="editOrSaveMealType(index)" aria-hidden="true"></i
></span>
</h5>
</div>
@ -107,23 +96,16 @@
<input class="form-control" placeholder="Name" v-model="meal_type.name" />
</div>
<div class="form-group">
<emoji-input :field="'icon'" :label="$t('Icon')"
:value="meal_type.icon"></emoji-input>
<emoji-input :field="'icon'" :label="$t('Icon')" :value="meal_type.icon"></emoji-input>
</div>
<div class="form-group">
<label>{{ $t("Color") }}</label>
<input class="form-control" type="color" name="Name"
:value="meal_type.color"
@change="meal_type.color = $event.target.value"/>
<input class="form-control" type="color" name="Name" :value="meal_type.color" @change="meal_type.color = $event.target.value" />
</div>
<b-form-checkbox id="checkbox-1" v-model="meal_type.default"
name="default_checkbox" class="mb-2">
<b-form-checkbox id="checkbox-1" v-model="meal_type.default" name="default_checkbox" class="mb-2">
{{ $t("Default") }}
</b-form-checkbox>
<button class="btn btn-danger" @click="deleteMealType(index)">{{
$t("Delete")
}}
</button>
<button class="btn btn-danger" @click="deleteMealType(index)">{{ $t("Delete") }}</button>
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">
{{ $t("Save") }}
</button>
@ -147,15 +129,16 @@
openEntryEdit(contextData.originalItem.entry)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{
$t("Edit")
}}</a>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{ $t("Edit") }}</a>
</ContextMenuItem>
<ContextMenuItem
v-if="contextData.originalItem.entry.recipe != null"
@click="$refs.menu.close();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>
v-if="contextData && contextData.originalItem && contextData.originalItem.entry.recipe != null"
@click="
$refs.menu.close()
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
@click="
@ -163,8 +146,7 @@
moveEntryLeft(contextData)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i>
{{ $t("Move") }}</a>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i> {{ $t("Move") }}</a>
</ContextMenuItem>
<ContextMenuItem
@click="
@ -172,8 +154,7 @@
moveEntryRight(contextData)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i>
{{ $t("Move") }}</a>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i> {{ $t("Move") }}</a>
</ContextMenuItem>
<ContextMenuItem
@click="
@ -189,8 +170,7 @@
addToShopping(contextData)
"
>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i>
{{ $t("Add_to_Shopping") }}</a>
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
</ContextMenuItem>
<ContextMenuItem
@click="
@ -198,15 +178,12 @@
deleteEntry(contextData)
"
>
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i>
{{ $t("Delete") }}</a>
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
</ContextMenuItem>
</template>
</ContextMenu>
<meal-plan-edit-modal
:entry="entryEditing"
:entryEditing_initial_recipe="entryEditing_initial_recipe"
:entry-editing_initial_meal_type="entryEditing_initial_meal_type"
:modal_title="modal_title"
:edit_modal_show="edit_modal_show"
@save-entry="editEntry"
@ -233,7 +210,8 @@
><i class="fas fa-external-link-alt"></i>
{{ $t("Open") }}
</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") }}
</b-button>
</b-button-group>
@ -243,17 +221,12 @@
</div>
</template>
<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)"
v-if="current_tab === 0">
<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">
<div class="col-md-3 col-6">
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
</button>
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i class="fas fa-calendar-plus"></i> {{ $t("Create") }}</button>
</div>
<div class="col-md-3 col-6">
<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>
<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>
</div>
<div class="col-md-3 col-6">
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
@ -262,27 +235,23 @@
</a>
</div>
<div class="col-md-3 col-6">
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top
:title="$t('Coming_Soon')">
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top :title="$t('Coming_Soon')">
{{ $t("Auto_Planner") }}
</button>
</div>
<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-group class="mx-1">
<b-button v-html="'<<'"
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
<b-button v-html="'<<'" @click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
<b-button v-html="'<'" @click="setStartingDay(-1)"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
class="fas fa-home"></i></b-button>
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i class="fas fa-home"></i></b-button>
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>'" @click="setStartingDay(1)"></b-button>
<b-button v-html="'>>'"
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
<b-button v-html="'>>'" @click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
</b-button-group>
</b-button-toolbar>
</div>
@ -385,20 +354,6 @@ export default {
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 () {
let items = []
this.plan_entries.forEach((entry) => {
@ -455,7 +410,7 @@ export default {
},
methods: {
openRecipe: function (recipe) {
window.open(this.resolveDjangoUrl('view_recipe', recipe.id))
window.open(this.resolveDjangoUrl("view_recipe", recipe.id))
},
addToShopping(entry) {
if (entry.originalItem.entry.recipe !== null) {

View File

@ -161,7 +161,7 @@
:flat="true"
:auto-load-root-options="false"
searchNested
:placeholder="$t('Ingredients')"
:placeholder="$t('Foods')"
:normalizer="normalizer"
@input="refreshData(false)"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@ -457,7 +457,7 @@ export default {
this.pagination_count = result.data.count
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 meal plans are being shown - filter out any meal plan recipes from the recipe list
let mealPlans = []

View File

@ -93,6 +93,7 @@
:servings="servings"
:header="true"
@checked-state-changed="updateIngredientCheckedState"
@change-servings="servings = $event"
/>
</div>
@ -274,5 +275,4 @@ export default {
#app > div > div {
break-inside: avoid;
}
</style>

View File

@ -123,7 +123,7 @@
<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 -->
<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">
<ShoppingLineItem
:entries="entries[1]"
@ -190,6 +190,9 @@
<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>
</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>
<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>
@ -432,7 +435,10 @@
<div class="col col-md-6 text-right">
<generic-multiselect
size="sm"
@change="settings.shopping_share = $event.val;saveSettings()"
@change="
settings.shopping_share = $event.val
saveSettings()
"
:model="Models.USER"
:initial_selection="settings.shopping_share"
label="username"
@ -557,6 +563,26 @@
</div>
</b-tab>
</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')">
<div>
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
@ -637,26 +663,7 @@
</ContextMenuItem>
</template>
</ContextMenu>
<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>
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)" :modal_id="new_recipe.id" @finish="finishShopping" />
<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>
</template>
@ -908,6 +915,7 @@ export default {
},
},
mounted() {
console.log(screen.height)
this.getShoppingList()
this.getSupermarkets()
this.getShoppingCategories()
@ -1096,7 +1104,6 @@ export default {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
console.log(this.items)
} else {
console.log("no data returned")
}
@ -1399,11 +1406,23 @@ export default {
window.removeEventListener("offline", this.updateOnlineStatus)
},
addRecipeToShopping() {
console.log(this.new_recipe)
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
},
finishShopping() {
this.add_recipe_servings = 1
this.new_recipe = { id: undefined }
this.edit_recipe_list = undefined
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: {
hover: {
@ -1465,14 +1484,25 @@ export default {
font-size: 20px;
}
@media (max-width: 768px) {
@media screen and (max-width: 768px) {
#shoppinglist {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: scroll;
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;
}
}

View File

@ -3,7 +3,7 @@
v-model="selected_objects"
:options="objects"
:close-on-select="true"
:clear-on-select="true"
:clear-on-select="multiple"
:hide-selected="multiple"
:preserve-search="true"
:internal-search="false"
@ -35,7 +35,7 @@ export default {
// this.Models and this.Actions inherited from ApiMixin
loading: false,
objects: [],
selected_objects: [],
selected_objects: undefined,
}
},
props: {
@ -48,7 +48,7 @@ export default {
},
label: { type: String, default: "name" },
parent_variable: { type: String, default: undefined },
limit: { type: Number, default: 10 },
limit: { type: Number, default: 25 },
sticky_options: {
type: Array,
default() {
@ -61,6 +61,10 @@ export default {
return []
},
},
initial_single_selection: {
type: Object,
default: undefined,
},
multiple: { type: Boolean, default: true },
allow_create: { type: Boolean, default: false },
create_placeholder: { type: String, default: "You Forgot to Add a Tag Placeholder" },
@ -71,18 +75,37 @@ export default {
// watch it
this.selected_objects = newVal
},
initial_single_selection: function (newVal, oldVal) {
// watch it
this.selected_objects = newVal
},
clear: function (newVal, oldVal) {
if (this.multiple || !this.initial_single_selection) {
this.selected_objects = []
} else {
this.selected_objects = undefined
}
},
},
mounted() {
this.search("")
if (this.multiple || !this.initial_single_selection) {
this.selected_objects = this.initial_selection
} else {
this.selected_objects = this.initial_single_selection
}
},
computed: {
lookupPlaceholder() {
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: {
// this.genericAPI inherited from ApiMixin
@ -95,8 +118,9 @@ export default {
}
this.genericAPI(this.model, this.Actions.LIST, options).then((result) => {
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) => {
// select default items when present in object
if ("default" in item) {
if (item.default) {
if (this.multiple) {
@ -109,6 +133,7 @@ export default {
}
})
}
// this.removeMissingItems() # This removes items that are on another page of results
})
},
selectionChanged: function () {
@ -121,6 +146,13 @@ export default {
this.search("")
}, 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>

View File

@ -7,7 +7,7 @@
</template>
<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-primary" v-if="!ingredient.checked"></i>
</td>
@ -39,9 +39,9 @@
variant="link"
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
:class="{
'text-success': shopping_status === true,
'text-muted': shopping_status === false,
'text-warning': shopping_status === null,
'text-success': ingredient.shopping_status === true,
'text-muted': ingredient.shopping_status === false,
'text-warning': ingredient.shopping_status === null,
}"
/>
<span v-if="!ingredient.food.ignore_shopping" class="px-2">
@ -64,111 +64,49 @@ export default {
ingredient: Object,
ingredient_factor: { type: Number, default: 1 },
detailed: { type: Boolean, default: true },
recipe_list: { type: Number }, // ShoppingListRecipe ID, to filter ShoppingStatus
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],
data() {
return {
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
dirty: undefined,
}
},
watch: {
ShoppingListAndFilter: {
immediate: true,
handler(newVal, oldVal) {
// this whole sections is overly complicated
// trying to infer status of shopping for THIS recipe and THIS ingredient
// without know which recipe it is.
// 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
}
ingredient: {
handler() {},
deep: true,
},
"ingredient.shop": function (newVal) {
this.shop = newVal
},
},
mounted() {
this.shop = this.ingredient?.shop
},
mounted() {},
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() {
if (this.shopping_status == false) {
if (this.ingredient?.shopping_status == false) {
return this.$t("NotInShopping", { food: this.ingredient.food.name })
} else {
let list = this.shopping_list.filter((x) => x.food.id == this.ingredient.food.id)
let category = this.$t("Category") + ": " + this.ingredient?.food?.supermarket_category?.name ?? this.$t("Undefined")
let category = this.$t("Category") + ": " + this.ingredient?.category ?? this.$t("Undefined")
let popover = []
list.forEach((x) => {
;(this.ingredient?.shopping_list ?? []).forEach((x) => {
popover.push(
[
"<tr style='border-bottom: 1px solid #ccc'>",
"<td style='padding: 3px;'><em>",
x?.recipe_mealplan?.name ?? "",
x?.mealplan ?? "",
"</em></td>",
"<td style='padding: 3px;'>",
x?.amount ?? "",
"</td>",
"<td style='padding: 3px;'>",
x?.unit?.name ?? "" + "</td>",
x?.unit ?? "" + "</td>",
"<td style='padding: 3px;'>",
x?.food?.name ?? "",
x?.food ?? "",
"</td></tr>",
].join("")
)

View File

@ -13,7 +13,7 @@
</h4>
</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">
<b-form-select v-model="selected_shoppingrecipe" :options="ShoppingRecipes" size="sm"></b-form-select>
</div>
@ -31,14 +31,11 @@
</tr>
<template v-for="i in s.ingredients">
<ingredient-component
:ingredient="i"
:ingredient="prepareIngredient(i)"
:ingredient_factor="ingredient_factor"
:key="i.id"
:show_shopping="show_shopping"
:shopping_list="shopping_list"
:add_shopping_mode="add_shopping_mode"
:detailed="detailed"
:recipe_list="selected_shoppingrecipe"
@checked-state-changed="$emit('checked-state-changed', $event)"
@add-to-shopping="addShopping($event)"
/>
@ -59,6 +56,7 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
import IngredientComponent from "@/components/IngredientComponent"
import { ApiMixin, StandardToasts } from "@/utils/utils"
import ShoppingListViewVue from "../apps/ShoppingListView/ShoppingListView.vue"
Vue.use(BootstrapVue)
@ -79,6 +77,7 @@ export default {
detailed: { type: Boolean, default: true },
header: { type: Boolean, default: false },
add_shopping_mode: { type: Boolean, default: false },
recipe_list: { type: Number, default: undefined },
},
data() {
return {
@ -107,13 +106,14 @@ export default {
watch: {
ShoppingRecipes: function (newVal, oldVal) {
if (newVal.length === 0 || this.add_shopping_mode) {
this.selected_shoppingrecipe = undefined
this.selected_shoppingrecipe = this.recipe_list
} else if (newVal.length === 1) {
this.selected_shoppingrecipe = newVal[0].value
}
},
selected_shoppingrecipe: function (newVal, oldVal) {
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() {
@ -140,13 +140,31 @@ export default {
}
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params).then((result) => {
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) {
let servings = this.servings
if (del_shopping) {
servings = 0
servings = -1
}
let params = {
id: this.recipe,
@ -155,7 +173,7 @@ export default {
servings: servings,
}
this.genericAPI(this.Models.RECIPE, this.Actions.SHOPPING, params)
.then(() => {
.then((result) => {
if (del_shopping) {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
} else if (this.selected_shoppingrecipe) {
@ -164,13 +182,6 @@ export default {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}
})
.then(() => {
if (!this.add_shopping_mode) {
return this.getShopping(false)
} else {
this.$emit("shopping-added")
}
})
.catch((err) => {
if (del_shopping) {
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
@ -186,13 +197,51 @@ export default {
// ALERT: this will all break if ingredients are re-used between recipes
if (e.add) {
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 {
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) {
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>

View File

@ -71,9 +71,7 @@ export default {
image_placeholder: window.IMAGE_PLACEHOLDER,
}
},
mounted() {
console.log(this.value)
},
mounted() {},
computed: {
entry: function () {
return this.value.originalItem

View File

@ -25,7 +25,7 @@
<b-form-group>
<generic-multiselect
@change="selectRecipe"
:initial_selection="entryEditing_initial_recipe"
:initial_single_selection="entryEditing.recipe"
:label="'name'"
:model="Models.RECIPE"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@ -45,7 +45,7 @@
v-bind:placeholder="$t('Meal_Type')"
:limit="10"
:multiple="false"
:initial_selection="entryEditing_initial_meal_type"
:initial_single_selection="entryEditing.meal_type"
:allow_create="true"
:create_placeholder="$t('Create_New_Meal_Type')"
@new="createMealType"
@ -76,13 +76,16 @@
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
</b-form-group>
<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>
</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 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>
<ingredients-card v-if="entryEditing.recipe && entryEditing.addshopping" :recipe="entryEditing.recipe" :detailed="false"></ingredients-card>
<recipe-card v-if="entryEditing.recipe" :recipe="entryEditing.recipe" :detailed="false"></recipe-card>
</div>
</div>
<div class="row mt-3 mb-3">
@ -100,6 +103,7 @@
<script>
import Vue from "vue"
import VueCookies from "vue-cookies"
import { BootstrapVue } from "bootstrap-vue"
import GenericMultiselect from "@/components/GenericMultiselect"
import { ApiMixin, getUserPreference } from "@/utils/utils"
@ -108,13 +112,13 @@ const { ApiApiFactory } = require("@/utils/openapi/api")
const { StandardToasts } = require("@/utils/utils")
Vue.use(BootstrapVue)
Vue.use(VueCookies)
let MEALPLAN_COOKIE_NAME = "mealplan_settings"
export default {
name: "MealPlanEditModal",
props: {
entry: Object,
entryEditing_initial_recipe: Array,
entryEditing_initial_meal_type: Array,
entryEditing_inital_servings: Number,
modal_title: String,
modal_id: {
@ -130,7 +134,6 @@ export default {
components: {
GenericMultiselect,
RecipeCard: () => import("@/components/RecipeCard.vue"),
IngredientsCard: () => import("@/components/IngredientsCard.vue"),
},
data() {
return {
@ -138,18 +141,36 @@ export default {
missing_recipe: false,
missing_meal_type: false,
default_plan_share: [],
mealplan_settings: {
addshopping: false,
reviewshopping: false,
},
}
},
watch: {
entry: {
handler() {
this.entryEditing = Object.assign({}, this.entry)
if (this.entryEditing_inital_servings) {
this.entryEditing.servings = this.entryEditing_inital_servings
}
},
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 () {},
computed: {
@ -159,6 +180,9 @@ export default {
},
methods: {
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()
apiClient.listUserPreferences().then((result) => {
@ -181,8 +205,10 @@ export default {
cancel = true
}
if (!cancel) {
console.log("saving", { ...this.mealplan_settings, ...this.entryEditing })
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() {

View File

@ -1,6 +1,6 @@
<template>
<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
><h4>{{ $t("Add_Servings_to_Shopping", { servings: recipe_servings }) }}</h4></template
>
@ -16,10 +16,11 @@
:recipe="recipe.id"
:ingredient_factor="ingredient_factor"
:servings="recipe_servings"
:show_shopping="true"
:add_shopping_mode="true"
:recipe_list="list_recipe"
:header="false"
@add-to-shopping="addShopping($event)"
@starting-cart="add_shopping = $event"
/>
</b-collapse>
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
@ -34,10 +35,11 @@
:recipe="r.recipe.id"
:ingredient_factor="ingredient_factor"
:servings="recipe_servings"
:show_shopping="true"
:add_shopping_mode="true"
:recipe_list="list_recipe"
:header="false"
@add-to-shopping="addShopping($event)"
@starting-cart="add_shopping = [...add_shopping, ...$event]"
/>
</b-collapse>
</b-card>
@ -46,7 +48,8 @@
</b-card>
</div>
<b-input-group class="my-3">
<template #modal-footer="">
<b-input-group class="mr-3">
<b-input-group-prepend is-text>
{{ $t("Servings") }}
</b-input-group-prepend>
@ -58,6 +61,7 @@
<b-button variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
</b-input-group-append>
</b-input-group>
</template>
</b-modal>
</div>
</template>
@ -80,6 +84,8 @@ export default {
recipe: { required: true, type: Object },
servings: { type: Number, default: undefined },
modal_id: { required: true, type: Number },
mealplan: { type: Number, default: undefined },
list_recipe: { type: Number, default: undefined },
},
data() {
return {
@ -106,7 +112,6 @@ export default {
deep: true,
},
servings: function (newVal) {
console.log(newVal)
this.recipe_servings = parseInt(newVal)
},
},
@ -121,14 +126,6 @@ export default {
this.steps = result.data.steps
// 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
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) {
this.recipe_servings = result.data?.servings
}
@ -155,18 +152,20 @@ export default {
})
return Promise.all(promises)
})
.then(() => {
this.add_shopping = [
...this.add_shopping,
...this.related_recipes
.map((x) => x.steps)
.flat()
.map((x) => x.ingredients)
.flat()
.filter((x) => !x.food.override_ignore)
.map((x) => x.id),
]
})
// .then(() => {
// if (!this.list_recipe) {
// this.add_shopping = [
// ...this.add_shopping,
// ...this.related_recipes
// .map((x) => x.steps)
// .flat()
// .map((x) => x.ingredients)
// .flat()
// .filter((x) => !x.food.override_ignore)
// .map((x) => x.id),
// ]
// }
// })
})
},
addShopping: function (e) {
@ -182,6 +181,8 @@ export default {
id: this.recipe.id,
ingredients: this.add_shopping,
servings: this.recipe_servings,
mealplan: this.mealplan,
list_recipe: this.list_recipe,
}
let apiClient = new ApiApiFactory()
apiClient

View File

@ -49,7 +49,7 @@
<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>
<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>
<div class="row">
@ -65,9 +65,7 @@
<meal-plan-edit-modal
:entry="entryEditing"
:entryEditing_initial_recipe="[recipe]"
:entryEditing_inital_servings="servings_value"
:entry-editing_initial_meal_type="[]"
@save-entry="saveMealPlan"
:modal_id="`modal-meal-plan_${modal_id}`"
:allow_delete="false"
@ -118,6 +116,7 @@ export default {
},
},
entryEditing: {},
mealplan: undefined,
}
},
props: {
@ -147,12 +146,19 @@ export default {
},
saveMealPlan: function (entry) {
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()
apiClient
.createMealPlan(entry)
.then((result) => {
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)
})
.catch((error) => {
@ -163,7 +169,9 @@ export default {
this.entryEditing = this.options.entryEditing
this.entryEditing.recipe = this.recipe
this.entryEditing.date = moment(new Date()).format("YYYY-MM-DD")
this.$nextTick(function () {
this.$bvModal.show(`modal-meal-plan_${this.modal_id}`)
})
},
createShareLink: function () {
axios

View File

@ -1,43 +1,36 @@
<template>
<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-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>
<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)">
<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">
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 cols="1" class="px-1 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]
}}
<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] }} &ensp;
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">
{{ x[1] }} &ensp;
{{ x[0] }}
</div>
</b-col>
@ -45,27 +38,20 @@
<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>
<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-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>
<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"/>
<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">
@ -85,7 +71,8 @@
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)">
@mouseover="openRecipeCard($event, e)"
>
{{ formatOneRecipe(e) }}
</button>
</b-col>
@ -96,30 +83,24 @@
</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 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)">
<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">
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"/>
<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">
@ -134,12 +115,8 @@
</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 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" />
@ -269,7 +246,7 @@ export default {
}
return Intl.DateTimeFormat(window.navigator.language, {
dateStyle: "short",
timeStyle: "short"
timeStyle: "short",
}).format(Date.parse(datetime))
},
formatOneAmount: function (item) {
@ -344,11 +321,11 @@ export default {
/* border-bottom: 1px solid #000; /* …and with a border on the top */
/* } */
.checkbox-control {
font-size: 0.6rem
font-size: 0.6rem;
}
.checkbox-control-mobile {
font-size: 1rem
font-size: 1rem;
}
.rotate {

View File

@ -294,5 +294,7 @@
"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.",
"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"
}