Compare commits

...

23 Commits

Author SHA1 Message Date
vabene1111
28e554d04b badge when list selected 2025-12-21 14:57:29 +01:00
vabene1111
5040caf91c fixed another mealie edge case 2025-12-20 18:32:16 +01:00
vabene1111
c5fcfd07a7 quick optimizations 2025-12-05 08:20:19 +01:00
vabene1111
a1a172e223 WIP 2025-12-04 09:25:16 +01:00
vabene1111
0039654d40 default list when adding trough mealplan/recipe 2025-12-04 08:07:32 +01:00
vabene1111
17de37b9fc fixed category change entry updaet 2025-12-04 07:46:56 +01:00
vabene1111
d0856ce3b7 category change fix 2025-12-04 07:19:58 +01:00
vabene1111
24426c2b7e Merge pull request #4282 from TandoorRecipes/dependabot/pip/django-5.2.9
Bump django from 5.2.8 to 5.2.9
2025-12-03 21:24:45 +01:00
vabene1111
5380b7d697 fixesa 2025-12-03 21:22:49 +01:00
vabene1111
1d0488fbb0 temporary fix 2025-12-03 18:09:05 +01:00
vabene1111
2213346297 Merge branch 'feature/shopping' into develop
# Conflicts:
#	vue3/src/locales/nl.json
#	vue3/src/locales/sl.json
#	vue3/src/locales/uk.json
2025-12-03 17:53:17 +01:00
vabene1111
e1a9938c0b works 2025-12-03 17:52:49 +01:00
dependabot[bot]
126f21842f Bump django from 5.2.8 to 5.2.9
Bumps [django](https://github.com/django/django) from 5.2.8 to 5.2.9.
- [Commits](https://github.com/django/django/compare/5.2.8...5.2.9)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-03 14:22:12 +00:00
vabene1111
9dfc9e1020 update shopping performance 2025-12-02 17:19:35 +01:00
SerhiiOS
84898b09f2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (874 of 874 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/uk/
2025-12-02 08:03:46 +00:00
Matjaž T.
d4ded02c2a Translated using Weblate (Slovenian)
Currently translated at 100.0% (874 of 874 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2025-12-02 08:03:46 +00:00
Justin Straver
8db294255e Translated using Weblate (Dutch)
Currently translated at 99.8% (873 of 874 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/
2025-12-02 08:03:46 +00:00
SerhiiOS
c99d13e2e7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (488 of 488 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/uk/
2025-12-02 08:03:45 +00:00
vabene1111
5608f80246 lots of WIP stuff 2025-12-01 18:39:40 +01:00
Matjaž T.
59040f4cdf Translated using Weblate (Slovenian)
Currently translated at 100.0% (871 of 871 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sl/
2025-12-01 06:08:34 +00:00
Matjaž T.
af476480c1 Translated using Weblate (Slovenian)
Currently translated at 100.0% (373 of 373 strings)

Translation: Tandoor/Recipes Backend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/sl/
2025-12-01 06:08:34 +00:00
vabene1111
9307e61c1a playing with display 2025-11-30 16:07:18 +01:00
vabene1111
8f5593d5ca shopping lists in SLE dialog 2025-11-30 15:32:54 +01:00
72 changed files with 5097 additions and 4514 deletions

View File

@@ -177,8 +177,9 @@ class RecipeShoppingEditor():
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing)
entries = []
for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create(
entry = ShoppingListEntry(
list_recipe=self._shopping_list_recipe,
food=i.food,
unit=i.unit,
@@ -187,6 +188,12 @@ class RecipeShoppingEditor():
created_by=self.created_by,
space=self.space,
)
entries.append(entry)
ShoppingListEntry.objects.bulk_create(entries)
for e in entries:
if e.food.shopping_lists.count() > 0:
e.shopping_lists.set(e.food.shopping_lists.all())
# deletes shopping list entries not in ingredients list
def _delete_ingredients(self, ingredients=None):

View File

@@ -197,7 +197,7 @@ class Mealie1(Integration):
space=self.request.space,
)
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
elif i['note'].strip():
elif i['note'] and i['note'].strip():
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-22 20:15+0200\n"
"PO-Revision-Date: 2025-11-15 12:08+0000\n"
"PO-Revision-Date: 2025-12-01 06:08+0000\n"
"Last-Translator: \"Matjaž T.\" <matjaz@moj-svet.si>\n"
"Language-Team: Slovenian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/sl/>\n"
@@ -2129,9 +2129,8 @@ msgid ""
"please consult the django documentation on how to reset passwords."
msgstr ""
"Nastavitveno stran lahko uporabite samo za ustvarjanje prvega "
"uporabnika! \n"
" Če ste pozabili svoje poverilnice superuporabnika, si oglejte "
"dokumentacijo django o tem, kako ponastaviti gesla."
"uporabnika! Če ste pozabili poverilnice superuporabnika, "
"si oglejte dokumentacijo django za ponastavitev gesel."
#: .\cookbook\views\views.py:304
msgid "Passwords dont match!"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-01 15:04+0200\n"
"PO-Revision-Date: 2025-11-22 20:03+0000\n"
"PO-Revision-Date: 2025-12-02 08:03+0000\n"
"Last-Translator: SerhiiOS <serhios@users.noreply.translate.tandoor.dev>\n"
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/uk/>\n"
@@ -468,7 +468,7 @@ msgstr "Пошук"
#: .\cookbook\models.py:455 .\cookbook\templates\base.html:114
#: .\cookbook\templates\meal_plan.html:7
msgid "Meal-Plan"
msgstr "План харчування"
msgstr "Меню"
#: .\cookbook\models.py:456 .\cookbook\templates\base.html:122
#: .\cookbook\views\views.py:459
@@ -2790,7 +2790,7 @@ msgstr ""
#: .\cookbook\views\views.py:451
msgid "Manage recipes, shopping list, meal plans and more."
msgstr "Керуйте рецептами, списком покупок, планами харчування тощо."
msgstr "Керуйте рецептами, списком покупок, меню тощо."
#: .\cookbook\views\views.py:458
msgid "Plan"

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.7 on 2025-11-30 14:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0232_shoppinglist'),
]
operations = [
migrations.AddField(
model_name='food',
name='shopping_lists',
field=models.ManyToManyField(blank=True, to='cookbook.shoppinglist'),
),
migrations.AddField(
model_name='shoppinglistentry',
name='shopping_lists',
field=models.ManyToManyField(blank=True, to='cookbook.shoppinglist'),
),
migrations.AddField(
model_name='supermarket',
name='shopping_lists',
field=models.ManyToManyField(blank=True, to='cookbook.shoppinglist'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.8 on 2025-12-03 16:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0233_food_shopping_lists_shoppinglistentry_shopping_lists_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='shoppinglist',
options={'ordering': ('pk',)},
),
migrations.AddField(
model_name='userpreference',
name='shopping_update_food_lists',
field=models.BooleanField(default=True),
),
]

View File

@@ -551,6 +551,7 @@ class UserPreference(models.Model, PermissionModelMixin):
show_step_ingredients = models.BooleanField(default=True)
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
shopping_recent_days = models.PositiveIntegerField(default=7)
shopping_update_food_lists = models.BooleanField(default=True)
csv_delim = models.CharField(max_length=2, default=",")
csv_prefix = models.CharField(max_length=10, blank=True, )
@@ -666,6 +667,7 @@ class Supermarket(models.Model, PermissionModelMixin):
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True)
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
shopping_lists = models.ManyToManyField("ShoppingList", blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
@@ -780,6 +782,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
url = models.CharField(max_length=1024, blank=True, null=True, default='')
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL) # inherited field
shopping_lists = models.ManyToManyField("ShoppingList", blank=True)
ignore_shopping = models.BooleanField(default=False) # inherited field
onhand_users = models.ManyToManyField(User, blank=True)
description = models.TextField(default='', blank=True)
@@ -943,8 +946,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
def __str__(self):
return f'{self.pk}: {self.amount} ' + (self.food.name if self.food else ' ') + (self.unit.name if self.unit else '')
# def __str__(self):
# return f'{self.pk}: {self.amount} ' + (self.food.name if self.food else ' ') + (self.unit.name if self.unit else '')
class Meta:
ordering = ['order', 'pk']
@@ -1297,8 +1300,8 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
objects = ScopedManager(space='space')
def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}'
# def __str__(self):
# return f'Shopping list recipe {self.id} - {self.recipe}'
class Meta:
ordering = ('pk',)
@@ -1315,8 +1318,12 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
class Meta:
ordering = ('pk',)
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
shopping_lists = models.ManyToManyField(ShoppingList, blank=True)
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')
food = models.ForeignKey(Food, on_delete=models.CASCADE, related_name='shopping_entries')
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)

View File

@@ -1,3 +1,4 @@
import traceback
import uuid
from datetime import datetime, timedelta
from decimal import Decimal
@@ -186,11 +187,37 @@ class SpaceFilterSerializer(serializers.ListSerializer):
if isinstance(self.context['request'].user, AnonymousUser):
data = []
else:
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
iterable = data.all() if hasattr(data, 'all') else data
if isinstance(iterable, list) or (isinstance(iterable, QuerySet) and getattr(iterable, '_result_cache', None) is not None):
try:
new_data = []
for u in iterable:
for us in u.userspace_set.all():
if us.space.id == self.context['request'].space.id:
new_data.append(u)
data = new_data
except Exception:
traceback.print_exc()
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
else:
if hasattr(self.context['request'], 'space'):
data = data.filter(userspace__space=self.context['request'].space).all()
else:
# not sure why but this branch can be hit (just normal page load, need to see why)
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
elif isinstance(data, list):
data = [d for d in data if getattr(d, self.child.Meta.model.get_space_key()[0]) == self.context['request'].space]
else:
data = data.filter(**{'__'.join(self.child.Meta.model.get_space_key()): self.context['request'].space})
iterable = data.all() if hasattr(data, 'all') else data
if isinstance(iterable, list) or (isinstance(iterable, QuerySet) and getattr(iterable, '_result_cache', None) is not None):
keys = self.child.Meta.model.get_space_key()
if keys == ('space',):
data = [d for d in iterable if getattr(d, 'space_id') == self.context['request'].space.id]
else:
# use cached results here too, just dont have time to test this now, probably obj.get_space()
data = data.filter(**{'__'.join(self.child.Meta.model.get_space_key()): self.context['request'].space})
else:
data = data.filter(**{'__'.join(self.child.Meta.model.get_space_key()): self.context['request'].space})
return super().to_representation(data)
@@ -484,6 +511,20 @@ class SpacedModelSerializer(serializers.ModelSerializer):
return super().create(validated_data)
class ShoppingListSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
obj, created = ShoppingList.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
class Meta:
model = ShoppingList
fields = ('id', 'name', 'description', 'color',) # returning dates breaks breaks shopping list deviceSetting save due to date retrieved from local storage as string
read_only_fields = ('id',)
class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
@@ -533,7 +574,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
'csv_delim', 'csv_prefix',
'csv_delim', 'csv_prefix', 'shopping_update_food_lists',
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients',
'food_children_exist'
)
@@ -648,7 +689,7 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
@extend_schema_field(str)
def get_label(self, obj):
return str(obj)
return obj.name
class Meta:
list_serializer_class = SpaceFilterSerializer
@@ -665,7 +706,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
@extend_schema_field(str)
def get_label(self, obj):
return str(obj)
return obj.name
def create(self, validated_data):
# since multi select tags dont have id's
@@ -740,8 +781,9 @@ class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
fields = ('id', 'category', 'supermarket', 'order')
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataModelMixin):
class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, WritableNestedModelSerializer, OpenDataModelMixin):
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
shopping_lists = ShoppingListSerializer(many=True, required=False)
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
@@ -752,7 +794,7 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataMo
class Meta:
model = Supermarket
fields = ('id', 'name', 'description', 'category_to_supermarket', 'open_data_slug')
fields = ('id', 'name', 'description', 'shopping_lists', 'category_to_supermarket', 'open_data_slug')
class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
@@ -836,7 +878,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
parent = IntegerField(read_only=True)
shopping_lists = ShoppingListSerializer(many=True, required=False)
properties = PropertySerializer(many=True, allow_null=True, required=False)
properties_food_unit = UnitSerializer(allow_null=True, required=False)
properties_food_amount = CustomDecimalField(required=False)
@@ -947,7 +989,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url', 'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id',
'food_onhand', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug',
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand', 'child_inherit_fields', 'open_data_slug', 'shopping_lists',
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@@ -1327,7 +1369,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
@extend_schema_field(bool)
def in_shopping(self, obj):
return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists()
return obj.shoppinglistrecipe_set.count() > 0
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
@@ -1393,27 +1435,23 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingListRecipe
fields = ('id', 'name', 'recipe', 'recipe_data', 'mealplan', 'meal_plan_data', 'servings', 'created_by',)
fields = ('id', 'name', 'recipe', 'recipe_data', 'meal_plan_data', 'mealplan', 'servings', 'created_by',)
read_only_fields = ('id', 'created_by',)
class ShoppingListSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
space = validated_data.pop('space', self.context['request'].space)
obj, created = ShoppingList.objects.get_or_create(name__iexact=validated_data['name'], space=space, defaults=validated_data)
return obj
class FoodShoppingSerializer(serializers.ModelSerializer):
supermarket_category = SupermarketCategorySerializer(read_only=True)
shopping_lists = ShoppingListSerializer(read_only=True, many=True)
class Meta:
model = ShoppingList
fields = ('id', 'name', 'description', 'color', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_at', 'updated_at',)
model = Food
fields = ('id', 'name', 'plural_name', 'supermarket_category', 'shopping_lists')
class ShoppingListEntrySerializer(WritableNestedModelSerializer):
food = FoodSerializer(allow_null=True)
food = FoodShoppingSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True, required=False)
shopping_lists = ShoppingListSerializer(many=True, required=False)
list_recipe_data = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserSerializer(read_only=True)
@@ -1462,7 +1500,13 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
created_by=self.context['request'].user)
del validated_data['mealplan_id']
return super().create(validated_data)
obj = super().create(validated_data)
if self.context['request'].user.userpreference.shopping_update_food_lists:
obj.shopping_lists.clear()
obj.shopping_lists.set(obj.food.shopping_lists.all())
return obj
def update(self, instance, validated_data):
user = self.context['request'].user
@@ -1482,7 +1526,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
class Meta:
model = ShoppingListEntry
fields = (
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked', 'ingredient',
'id', 'list_recipe', 'shopping_lists', 'food', 'unit', 'amount', 'order', 'checked', 'ingredient',
'list_recipe_data', 'created_by', 'created_at', 'updated_at', 'completed_at', 'delay_until', 'mealplan_id'
)
read_only_fields = ('id', 'created_by', 'created_at')

View File

@@ -1992,19 +1992,22 @@ class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
if serializer.is_valid():
entries = []
for e in serializer.validated_data['entries']:
entries.append(
ShoppingListEntry(
list_recipe_id=obj.pk,
amount=e['amount'],
unit_id=e['unit_id'],
food_id=e['food_id'],
ingredient_id=e['ingredient_id'],
created_by_id=request.user.id,
space_id=request.space.id,
)
entry = ShoppingListEntry(
list_recipe_id=obj.pk,
amount=e['amount'],
unit_id=e['unit_id'],
food_id=e['food_id'],
ingredient_id=e['ingredient_id'],
created_by_id=request.user.id,
space_id=request.space.id,
)
entries.append(entry)
ShoppingListEntry.objects.bulk_create(entries)
for e in entries:
if e.food.shopping_lists.count() > 0:
e.shopping_lists.set(e.food.shopping_lists.all())
ConnectorManager.add_work(ActionType.CREATED, *entries)
return Response(serializer.validated_data)
else:
@@ -2041,19 +2044,23 @@ class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
# select_related("list_recipe")
self.queryset = self.queryset.filter(
Q(created_by=self.request.user)
| Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by', 'food',
'food__properties',
'food__properties__property_type',
'food__inherit_fields',
'food__supermarket_category',
'food__onhand_users',
'food__substitute',
'food__child_inherit_fields',
'unit', 'list_recipe',
| Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by',
'food',
'food__shopping_lists',
'shopping_lists',
'unit',
'list_recipe',
'list_recipe__recipe__keywords',
'list_recipe__recipe__created_by',
'list_recipe__mealplan',
'list_recipe__mealplan__shared',
'list_recipe__mealplan__shared__userspace_set',
'list_recipe__mealplan__shoppinglistrecipe_set',
'list_recipe__mealplan__recipe',
'list_recipe__mealplan__recipe__keywords',
).distinct().all()
updated_after = self.request.query_params.get('updated_after', None)

View File

@@ -1,4 +1,4 @@
Django==5.2.8
Django==5.2.9
cryptography===45.0.5
django-annoying==0.10.6
django-cleanup==9.0.0

View File

@@ -1,6 +1,6 @@
<template>
<v-dialog v-model="dialog" activator="parent" style="max-width: 75vw;">
<v-dialog v-model="dialog" :activator="props.activator" style="max-width: 75vw;">
<v-card>
<v-closable-card-title :title="$t('Export')" v-model="dialog"></v-closable-card-title>
@@ -48,7 +48,7 @@
<script setup lang="ts">
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {computed, ref} from "vue";
import {computed, PropType, ref, shallowRef} from "vue";
import {useShoppingStore} from "@/stores/ShoppingStore.ts";
import {isEntryVisible, isShoppingCategoryVisible, isShoppingListFoodVisible} from "@/utils/logic_utils.ts";
import {useI18n} from "vue-i18n";
@@ -56,10 +56,15 @@ import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {ShoppingListEntry} from "@/openapi";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import {useClipboard} from "@vueuse/core";
import {EditorSupportedModels, getGenericModelFromString} from "@/types/Models.ts";
const {t} = useI18n()
const {copy} = useClipboard()
const props = defineProps({
activator: {default: 'parent'},
})
const dialog = defineModel<boolean>()
const mode = ref<'md_list' | 'md_table' | 'csv'>('md_list')
@@ -78,7 +83,7 @@ const exportText = computed(() => {
textArray.push(formatHeader())
useShoppingStore().getEntriesByGroup.forEach(category => {
useShoppingStore().entriesByGroup.forEach(category => {
if (isShoppingCategoryVisible(category)) {
if (category.name === useShoppingStore().UNDEFINED_CATEGORY) {
textArray.push(formatCategory(t('NoCategory')))

View File

@@ -8,6 +8,9 @@
<v-label>{{ $t('Choose_Category') }}</v-label>
<model-select model="SupermarketCategory" @update:modelValue="categoryUpdate" allow-create></model-select>
<v-label>{{ $t('ShoppingList') }}</v-label>
<model-select model="ShoppingList" @update:modelValue="shoppingListUpdate" mode="tags" allow-create></model-select>
<v-row>
<v-col class="pr-0">
<v-btn height="80px" color="info" density="compact" size="small" block stacked
@@ -76,6 +79,9 @@
<v-list-item-subtitle v-if="isDelayed(e)" class="text-info font-weight-bold">
{{ $t('PostponedUntil') }} {{ DateTime.fromJSDate(e.delayUntil!).toLocaleString(DateTime.DATETIME_SHORT) }}
</v-list-item-subtitle>
<v-list-item-subtitle v-if="e.shoppingLists.length > 0" class="text-info font-weight-bold">
<shopping-lists-bar :shopping-lists="e.shoppingLists"></shopping-lists-bar>
</v-list-item-subtitle>
<v-btn-group divided border>
<v-btn icon="" @click="e.amount = e.amount / 2; updateEntryAmount(e)" v-if="!e.ingredient">
@@ -122,8 +128,8 @@
<script setup lang="ts">
import {computed} from "vue";
import {ApiApi, PatchedShoppingListEntry, ShoppingListEntry, SupermarketCategory} from "@/openapi";
import {computed, ref} from "vue";
import {ApiApi, PatchedShoppingListEntry, ShoppingList, ShoppingListEntry, SupermarketCategory} from "@/openapi";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {IShoppingListFood} from "@/types/Shopping";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
@@ -133,12 +139,16 @@ import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import {useShoppingStore} from "@/stores/ShoppingStore";
import {isDelayed, isShoppingListFoodDelayed} from "@/utils/logic_utils";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import ShoppingListsBar from "@/components/display/ShoppingListsBar.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const {mobile} = useDisplay()
const showDialog = defineModel<Boolean>()
const shoppingListFood = defineModel<IShoppingListFood>('shoppingListFood', {required: true})
const shoppingListUpdateLoading = ref(false)
/**
* returns a flat list of entries for the given shopping list food
*/
@@ -164,6 +174,8 @@ const isShoppingLineDelayed = computed(() => {
function categoryUpdate(category: SupermarketCategory) {
const api = new ApiApi()
shoppingListFood.value.food.supermarketCategory = category
shoppingListFood.value.entries.forEach(e => e.food.supermarketCategory = category)
useShoppingStore().updateEntriesStructure()
api.apiFoodUpdate({id: shoppingListFood.value.food.id, food: shoppingListFood.value.food}).then(r => {
useMessageStore().addPreparedMessage(PreparedMessage.UPDATE_SUCCESS)
}).catch(err => {
@@ -171,6 +183,35 @@ function categoryUpdate(category: SupermarketCategory) {
})
}
/**
* change the shopping list for all entries
* @param shoppingLists
*/
function shoppingListUpdate(shoppingLists: ShoppingList[]) {
const api = new ApiApi()
const promises: Promise<any>[] = []
shoppingListUpdateLoading.value = true
shoppingListFood.value.entries.forEach(e => {
e.shoppingLists = shoppingLists
promises.push(api.apiShoppingListEntryUpdate({id: e.id, shoppingListEntry: e}).then(r => {
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}))
})
if (useUserPreferenceStore().userSettings.shoppingUpdateFoodLists){
shoppingListFood.value.food.shoppingLists = shoppingLists
promises.push(api.apiFoodUpdate({id: shoppingListFood.value.food.id!, food: shoppingListFood.value.food}).then(r => {
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}))
}
Promise.all(promises).finally(() => shoppingListUpdateLoading.value = false)
}
/**
* add new entry for currently selected food type
*/

View File

@@ -1,11 +1,17 @@
<template>
<v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0" :id="itemContainerId" @touchend="handleSwipe()" @click="dialog = true;"
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)"
<v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0 shopping-border"
:id="itemContainerId"
@touchend="handleSwipe()"
@click="dialog = true;"
:value="shoppingListFood"
>
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">-->
<!-- <i class="swipe-icon fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
<!-- </div>-->
<div class="color-marker-container">
<span :style="{background: sl.color}" v-for="sl in shoppingList"></span>
</div>
<div class="flex-grow-1 p-2">
<div class="d-flex">
@@ -31,13 +37,18 @@
</div>
<template v-slot:[selectBtnSlot]="{ isSelected, select }" v-if="selectEnabled">
<v-list-item-action class="ps-3 pe-3" start>
<v-checkbox-btn :model-value="isSelected" @update:model-value="select" @click.native.stop=""></v-checkbox-btn>
</v-list-item-action>
</template>
<template v-slot:[checkBtnSlot]>
<div class="ps-3 pe-3" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);">
<v-btn color="success" size="large"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn>
</div>
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
</template>
<!-- <div class="swipe-action bg-primary justify-content-end">-->
@@ -56,20 +67,23 @@ import {computed, PropType, ref} from "vue";
import {DateTime} from "luxon";
import {useShoppingStore} from "@/stores/ShoppingStore.js";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.js";
import {ApiApi, Food, ShoppingListEntry} from '@/openapi'
import {ApiApi, Food, ShoppingList, ShoppingListEntry} from '@/openapi'
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping";
import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils";
import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue";
import {pluralString} from "@/utils/model_utils.ts";
import ShoppingListsBar from "@/components/display/ShoppingListsBar.vue";
const emit = defineEmits(['clicked'])
const props = defineProps({
shoppingListFood: {type: {} as PropType<IShoppingListFood>, required: true},
hideInfoRow: {type: Boolean, default: false}
hideInfoRow: {type: Boolean, default: false},
selectEnabled: {type: Boolean, default: false}
})
const checkBtnSlot = ref(useUserPreferenceStore().userSettings.leftHanded ? 'prepend' : 'append')
const selectBtnSlot = ref(useUserPreferenceStore().userSettings.leftHanded ? 'append' : 'prepend')
const dialog = ref(false)
@@ -82,9 +96,7 @@ const entries = computed(() => {
*/
const itemContainerId = computed(() => {
let id = 'id_sli_'
for (let i in entries.value) {
id += i + '_'
}
entries.value.forEach(e => id += e.id + '_')
return id
})
@@ -112,6 +124,22 @@ const actionButtonIcon = computed(() => {
})
const shoppingList = computed(() => {
const lists = [] as ShoppingList[]
entries.value.forEach(e => {
if (e.shoppingLists) {
e.shoppingLists.forEach(l => {
if (lists.findIndex(sl => sl.id == l.id) == -1) {
lists.push(l)
}
})
}
})
return lists
})
/**
* calculate the amounts for the given line
* can combine 1 to n entries with the same unit
@@ -123,34 +151,34 @@ const amounts = computed((): ShoppingLineAmount[] => {
for (let i in entries.value) {
let e = entries.value[i]
if (isEntryVisible(e, useUserPreferenceStore().deviceSettings)) {
let unit = -1
if (e.unit !== undefined && e.unit !== null) {
unit = e.unit.id!
}
if (e.amount > 0) {
let unit = -1
if (e.unit !== undefined && e.unit !== null) {
unit = e.unit.id!
}
let uaMerged = false
unitAmounts.forEach(ua => {
if (((ua.unit == null && e.unit == null) || (ua.unit != null && ua.unit.id! == unit)) && ua.checked == e.checked && ua.delayed == isDelayed(e)) {
ua.amount += e.amount
uaMerged = true
}
})
if (e.amount > 0) {
if (!uaMerged) {
unitAmounts.push({
key: `${unit}_${e.checked}_${isDelayed(e)}`,
amount: e.amount,
unit: e.unit,
checked: e.checked,
delayed: isDelayed(e)
} as ShoppingLineAmount)
let uaMerged = false
unitAmounts.forEach(ua => {
if (((ua.unit == null && e.unit == null) || (ua.unit != null && ua.unit.id! == unit)) && ua.checked == e.checked && ua.delayed == isDelayed(e)) {
ua.amount += e.amount
uaMerged = true
}
})
if (!uaMerged) {
unitAmounts.push({
key: `${unit}_${e.checked}_${isDelayed(e)}`,
amount: e.amount,
unit: e.unit,
checked: e.checked,
delayed: isDelayed(e)
} as ShoppingLineAmount)
}
}
}
return unitAmounts
})
@@ -171,29 +199,28 @@ const infoRow = computed(() => {
for (let i in entries.value) {
let e = entries.value[i]
if (isEntryVisible(e, useUserPreferenceStore().deviceSettings)) {
if (authors.indexOf(e.createdBy.displayName) === -1) {
authors.push(e.createdBy.displayName)
}
if (e.listRecipe != null) {
if (e.listRecipeData.recipe != null) {
let recipe_name = e.listRecipeData.recipeData.name
if (recipes.indexOf(recipe_name) === -1) {
recipes.push(recipe_name.substring(0, 14) + (recipe_name.length > 14 ? '..' : ''))
}
}
if (e.listRecipeData.mealplan != null) {
let meal_plan_entry = (e.listRecipeData.mealPlanData.mealType.name.substring(0, 8) || '') + (e.listRecipeData.mealPlanData.mealType.name.length > 8 ? '..' : '') + ' (' + DateTime.fromJSDate(e.listRecipeData.mealPlanData.fromDate).toLocaleString(DateTime.DATE_SHORT) + ')'
if (meal_pans.indexOf(meal_plan_entry) === -1) {
meal_pans.push(meal_plan_entry)
}
}
}
if (authors.indexOf(e.createdBy.displayName) === -1) {
authors.push(e.createdBy.displayName)
}
if (e.listRecipe != null) {
if (e.listRecipeData.recipe != null) {
let recipe_name = e.listRecipeData.recipeData.name
if (recipes.indexOf(recipe_name) === -1) {
recipes.push(recipe_name.substring(0, 14) + (recipe_name.length > 14 ? '..' : ''))
}
}
if (e.listRecipeData.mealplan != null) {
let meal_plan_entry = (e.listRecipeData.mealPlanData.mealType.name.substring(0, 8) || '') + (e.listRecipeData.mealPlanData.mealType.name.length > 8 ? '..' : '') + ' (' + DateTime.fromJSDate(e.listRecipeData.mealPlanData.fromDate).toLocaleString(DateTime.DATE_SHORT) + ')'
if (meal_pans.indexOf(meal_plan_entry) === -1) {
meal_pans.push(meal_plan_entry)
}
}
}
}
if (useUserPreferenceStore().deviceSettings.shopping_item_info_created_by && authors.length > 0) {
@@ -247,4 +274,22 @@ function handleSwipe() {
<style>
/* TODO swipe system classes removed because not working (visually, touch detection was working), retrieve from old ShoppingLineItem VCS */
/* 2. Container to wrap the color bars and place them to the far left */
.color-marker-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 3px;
display: flex;
flex-direction: column;
}
.color-marker-container span {
width: 100%;
flex-grow: 1;
}
</style>

View File

@@ -2,7 +2,7 @@
<v-tabs v-model="currentTab">
<v-tab value="shopping"><i class="fas fa-fw"
:class="{'fa-circle-notch fa-spin':useShoppingStore().currentlyUpdating, 'fa-shopping-cart ': !useShoppingStore().currentlyUpdating}"></i> <span
class="d-none d-md-block ms-1">{{ $t('Shopping_list') }} ({{ useShoppingStore().stats.countUnchecked }})</span></v-tab>
class="d-none d-md-block ms-1">{{ $t('Shopping_list') }} ({{ useShoppingStore().totalFoods }})</span></v-tab>
<v-tab value="recipes"><i class="fas fa-book fa-fw"></i> <span class="d-none d-md-block ms-1">{{
$t('Recipes')
}} ({{ useShoppingStore().getAssociatedRecipes().length }})</span></v-tab>
@@ -25,9 +25,13 @@
<v-list density="compact">
<v-list-item @click="useShoppingStore().undoChange()" prepend-icon="fa-solid fa-arrow-rotate-left">{{ $t('Undo') }}</v-list-item>
<v-list-item @click="exportDialog = true" link prepend-icon="fa-solid fa-download">
{{ $t('Export') }}
</v-list-item>
<v-divider></v-divider>
<v-list-item>
<v-select hide-details :items="groupingOptionsItems" v-model="useUserPreferenceStore().deviceSettings.shopping_selected_grouping" :label="$t('GroupBy')">
<v-select hide-details :items="groupingOptionsItems" v-model="useUserPreferenceStore().deviceSettings.shopping_selected_grouping"
:label="$t('GroupBy')">
</v-select>
</v-list-item>
<v-list-item v-if="useUserPreferenceStore().deviceSettings.shopping_selected_grouping == ShoppingGroupingOptions.CATEGORY">
@@ -65,29 +69,94 @@
</v-list>
</v-menu>
<v-btn height="100%" rounded="0" variant="plain">
<i class="fa-solid fa-download"></i>
<shopping-export-dialog></shopping-export-dialog>
</v-btn>
<!-- <v-btn height="100%" rounded="0" variant="plain">-->
<!-- <i class="fa-solid fa-download"></i>-->
<!-- <shopping-export-dialog></shopping-export-dialog>-->
<!-- </v-btn>-->
<v-btn height="100%" rounded="0" variant="plain" @click="useShoppingStore().undoChange()">
<i class="fa-solid fa-arrow-rotate-left"></i>
</v-btn>
<!-- <v-btn height="100%" rounded="0" variant="plain" @click="useShoppingStore().undoChange()">-->
<!-- <i class="fa-solid fa-arrow-rotate-left"></i>-->
<!-- </v-btn>-->
</v-tabs>
<shopping-export-dialog v-model="exportDialog" activator="model"></shopping-export-dialog>
<v-window v-model="currentTab">
<v-window-item value="shopping">
<v-container>
<!-- <v-row class="pa-0" dense>-->
<!-- <v-col class="pa-0">-->
<!-- <v-chip-group v-model="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket" v-if="supermarkets.length > 0">-->
<!-- <v-chip v-for="s in supermarkets" :value="s" :key="s.id" label density="compact" variant="outlined" color="primary">{{ s.name }}</v-chip>-->
<!-- </v-chip-group>-->
<!-- </v-col>-->
<!-- </v-row>-->
<v-row class="pa-0" dense>
<v-col class="pa-0">
<v-chip-group v-model="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket" v-if="supermarkets.length > 0">
<v-chip v-for="s in supermarkets" :value="s" :key="s.id" label density="compact" variant="outlined" color="primary">{{ s.name }}</v-chip>
</v-chip-group>
<v-chip label density="compact" variant="outlined" style="max-width: 50%;" :prepend-icon="TSupermarket.icon" append-icon="fa-solid fa-caret-down">
<span v-if="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket != null">
{{ useUserPreferenceStore().deviceSettings.shopping_selected_supermarket.name }}
</span>
<span v-else>{{ $t('Supermarket') }}</span>
<v-menu activator="parent">
<v-list density="compact">
<v-list-item @click="useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list = []; useShoppingStore().updateEntriesStructure()">
{{ $t('SelectNone') }}
</v-list-item>
<v-list-item v-for="s in supermarkets" :key="s.id" @click="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket = s">
{{ s.name }}
</v-list-item>
<v-list-item prepend-icon="$create" :to="{name: 'ModelEditPage', params: {model: 'Supermarket'}}">
{{ $t('Create') }}
</v-list-item>
</v-list>
</v-menu>
</v-chip>
<v-chip label density="compact" class="ms-1" variant="outlined"
:color="(useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list.length == 0 ? '' : 'secondary')" :prepend-icon="TShoppingList.icon"
append-icon="fa-solid fa-caret-down">
<template v-if="useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list.filter(sl => sl != -1).length > 0">
{{
shoppingLists.filter(sl => useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list.includes(sl.id)).flatMap(sl => sl.name).join(', ')
}}
</template>
<template v-else>{{ $t('ShoppingList') }}</template>
<v-menu activator="parent" :close-on-content-click="false">
<v-list density="compact" v-model:selected="useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list" select-strategy="leaf">
<v-list-item @click="useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list = [] ">
{{ $t('All') }}
</v-list-item>
<v-list-item :value="-1" @click="useUserPreferenceStore().deviceSettings.shopping_selected_shopping_list = [-1];">
<template v-slot:prepend="{ isSelected, select }">
<v-list-item-action start>
<v-checkbox-btn :model-value="isSelected" @update:model-value="select"></v-checkbox-btn>
</v-list-item-action>
</template>
{{ $t('None') }}
</v-list-item>
<v-list-item v-for="s in shoppingLists" :key="s.id" :value="s.id">
<template v-slot:prepend="{ isSelected, select }">
<v-list-item-action start>
<v-checkbox-btn :model-value="isSelected" @update:model-value="select"></v-checkbox-btn>
</v-list-item-action>
</template>
{{ s.name }}
</v-list-item>
<v-list-item prepend-icon="$create" :to="{name: 'ModelEditPage', params: {model: 'ShoppingList'}}">
{{ $t('Create') }}
</v-list-item>
</v-list>
</v-menu>
</v-chip>
</v-col>
</v-row>
<v-row>
<v-row class="mt-0">
<v-col>
<v-alert v-if="useShoppingStore().hasFailedItems()" color="warning" class="mb-2">
<template #prepend>
@@ -107,19 +176,18 @@
<v-skeleton-loader type="list-item"></v-skeleton-loader>
<v-skeleton-loader type="list-item"></v-skeleton-loader>
</v-list>
<v-list class="mt-3" density="compact" v-else>
<template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name">
<template v-if="isShoppingCategoryVisible(category)">
<v-list class="mt-3" density="compact" v-model:selected="selectedLines" select-strategy="leaf" v-else>
<template v-for="category in useShoppingStore().entriesByGroup" :key="category.name">
<v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader>
<v-list-subheader v-else>{{ category.name }}</v-list-subheader>
<v-divider></v-divider>
<template v-for="[i, value] in category.foods" :key="value.food.id">
<shopping-line-item :shopping-list-food="value"></shopping-line-item>
</template>
<v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader>
<v-list-subheader v-else>{{ category.name }}</v-list-subheader>
<v-divider></v-divider>
<template v-for="[i, value] in category.foods" :key="value.food.id">
<shopping-line-item :shopping-list-food="value"></shopping-line-item>
</template>
</template>
</v-list>
@@ -246,14 +314,14 @@
<script setup lang="ts">
import {computed, onMounted, ref} from "vue";
import {computed, onMounted, ref, shallowRef, toRef, watch} from "vue";
import {useShoppingStore} from "@/stores/ShoppingStore";
import {ApiApi, Recipe, ResponseError, ShoppingListEntry, ShoppingListRecipe, Supermarket} from "@/openapi";
import {ApiApi, Recipe, ResponseError, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Supermarket} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {ShoppingGroupingOptions} from "@/types/Shopping";
import {IShoppingListFood, ShoppingGroupingOptions} from "@/types/Shopping";
import {useI18n} from "vue-i18n";
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue";
import SupermarketEditor from "@/components/model_editors/SupermarketEditor.vue";
@@ -265,13 +333,18 @@ import {onBeforeRouteLeave} from "vue-router";
import {isShoppingCategoryVisible} from "@/utils/logic_utils.ts";
import ShoppingExportDialog from "@/components/dialogs/ShoppingExportDialog.vue";
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
import {TShoppingList, TSupermarket} from "@/types/Models.ts";
const {t} = useI18n()
const exportDialog = ref(false)
const currentTab = ref("shopping")
const supermarkets = ref([] as Supermarket[])
const shoppingLists = ref([] as ShoppingList[])
const manualAddRecipe = ref<undefined | Recipe>(undefined)
const selectedLines = shallowRef([] as IShoppingListFood[])
/**
* VSelect items for shopping list grouping options with localized names
*/
@@ -283,6 +356,10 @@ const groupingOptionsItems = computed(() => {
return items
})
watch(() => useUserPreferenceStore().deviceSettings, () => {
useShoppingStore().updateEntriesStructure()
}, {deep: true})
onMounted(() => {
addEventListener("visibilitychange", (event) => {
useShoppingStore().autoSyncHasFocus = (document.visibilityState === 'visible')
@@ -304,6 +381,7 @@ onMounted(() => {
}
loadSupermarkets()
loadShoppingLists()
})
/**
@@ -378,6 +456,20 @@ function loadSupermarkets() {
})
}
/**
* load a list of supermarkets
*/
function loadShoppingLists() {
let api = new ApiApi()
api.apiShoppingListList().then(r => {
shoppingLists.value = r.results
// TODO either recursive or add a "favorite" attribute to supermarkets for them to display at all
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
</script>
<style scoped>

View File

@@ -0,0 +1,27 @@
<template>
<div v-if="props.shoppingLists">
<slot name="prepend"></slot>
<v-chip class="me-1 mb-1" :color="shoppingList.color" :size="props.size" :variant="props.variant" label v-for="shoppingList in props.shoppingLists">
{{ shoppingList.name }}
</v-chip>
<slot name="append"></slot>
</div>
</template>
<script setup lang="ts">
import {Keyword, KeywordLabel, ShoppingList} from "@/openapi";
import {computed, PropType} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const props = defineProps({
shoppingLists: Array as PropType<Array<ShoppingList> | undefined>,
size: {type: String, default: 'x-small'},
variant: {type: String as PropType<NonNullable<"tonal" | "flat" | "text" | "elevated" | "outlined" | "plain"> | undefined>, default: 'outlined'},
})
</script>

View File

@@ -30,6 +30,7 @@
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<!-- TODO fix card overflow invisible, overflow-visible class is not working -->
<model-select :label="$t('Category')" v-model="editingObj.supermarketCategory" model="SupermarketCategory" allow-create append-to-body></model-select>
<model-select :label="$t('ShoppingList')" :hint="$t('DefaultShoppingListHelp')" v-model="editingObj.shoppingLists" model="ShoppingList" mode="tags" allow-create append-to-body></model-select>
</v-form>
</v-tabs-window-item>

View File

@@ -27,6 +27,7 @@
<v-checkbox :label="$t('mealplan_autoinclude_related')" :hint="$t('mealplan_autoinclude_related_desc')" persistent-hint v-model="useUserPreferenceStore().userSettings.mealplanAutoincludeRelated"></v-checkbox>
<v-checkbox :label="$t('shopping_add_onhand')" :hint="$t('shopping_add_onhand_desc')" persistent-hint v-model="useUserPreferenceStore().userSettings.shoppingAddOnhand"></v-checkbox>
<v-checkbox :label="$t('filter_to_supermarket')" :hint="$t('filter_to_supermarket_desc')" persistent-hint v-model="useUserPreferenceStore().userSettings.filterToSupermarket"></v-checkbox>
<v-checkbox :label="$t('UpdateFoodLists')" :hint="$t('UpdateFoodListsHelp')" persistent-hint v-model="useUserPreferenceStore().userSettings.shoppingUpdateFoodLists"></v-checkbox>
<v-number-input
class="mt-2"

View File

@@ -23,6 +23,7 @@
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"All": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",

View File

@@ -23,6 +23,7 @@
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"All": "",
"App": "Приложение",
"Apply": "",
"Are_You_Sure": "Сигурен ли си?",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineació",
"All": "",
"Amount": "Quantitat",
"App": "Aplicació",
"Apply": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Zarovnání",
"All": "",
"Amount": "Množství",
"App": "Aplikace",
"Apply": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"All": "",
"Amount": "Mængde",
"App": "App",
"Apply": "",

View File

@@ -39,6 +39,7 @@
"AiProvider": "AI Anbieter",
"AiProviderHelp": "Je nach Präferenz können verschiedene AI Anbieter angelegt werden. Diese können auch Space übergreifend sein.",
"Alignment": "Ausrichtung",
"All": "Alle",
"AllRecipes": "Alle Rezepte",
"Amount": "Menge",
"App": "App",
@@ -151,6 +152,7 @@
"Decimals": "Nachkommastellen",
"Default": "Standard",
"DefaultPage": "Standardseite",
"DefaultShoppingListHelp": "Standard Liste wenn dieses Lebensmittel auf die Einkaufsliste gesetzt wird.",
"Default_Unit": "Standardeinheit",
"DelayFor": "Um {hours} Stunden verschieben",
"DelayUntil": "Verzögerung bis",
@@ -375,6 +377,7 @@
"NoUnit": "Keine Einheit",
"No_ID": "ID nicht gefunden und kann nicht gelöscht werden.",
"No_Results": "Keine Ergebnisse",
"None": "Keine",
"NotFound": "Nicht gefunden",
"NotFoundHelp": "Die gesuchte Seite konnte nicht gefunden werden.",
"NotInShopping": "{food} befindet sich nicht auf Ihrer Einkaufsliste.",
@@ -513,7 +516,7 @@
"Shopping_Categories": "Einkaufskategorien",
"Shopping_Category": "Einkaufskategorie",
"Shopping_List_Empty": "Deine Einkaufsliste ist aktuell leer. Einträge können über das Kontextmenü hinzugefügt werden (Rechtsklick auf einen Eintrag oder Klick auf das Menü-Icon)",
"Shopping_input_placeholder": "z.B. Kartoffeln/ 100 Kartoffeln/ 100 g Kartoffeln",
"Shopping_input_placeholder": "z.B. 100 g Kartoffeln",
"Shopping_list": "Einkaufsliste",
"ShowDelayed": "Zeige verschobene Elemente",
"ShowIngredients": "Zutaten anzeigen",
@@ -615,6 +618,8 @@
"Unrated": "Unbewertet",
"Up": "Hoch",
"Update": "Aktualisieren",
"UpdateFoodLists": "Lebensmittel Einkaufslisten aktualisieren",
"UpdateFoodListsHelp": "Aktualisiert die Standardeinkaufslisten im Lebensmittel wenn diese beim Einkaufen geändert werden.",
"Update_Existing_Data": "Vorhandene Daten aktualisieren",
"Updated": "Aktualisiert",
"UpgradeNow": "Jetzt Upgraden",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Ευθυγράμμιση",
"All": "",
"Amount": "Ποσότητα",
"App": "Εφαρμογή",
"Apply": "",

View File

@@ -37,6 +37,7 @@
"AiProvider": "AI Provider",
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
"Alignment": "Alignment",
"All": "All",
"AllRecipes": "All Recipes",
"Amount": "Amount",
"App": "App",
@@ -149,6 +150,7 @@
"Decimals": "Decimals",
"Default": "Default",
"DefaultPage": "Default Page",
"DefaultShoppingListHelp": "Default List when this Food is added to the Shoppinglist.",
"Default_Unit": "Default Unit",
"DelayFor": "Delay for {hours} hours",
"DelayUntil": "Delay Until",
@@ -373,6 +375,7 @@
"NoUnit": "No Unit",
"No_ID": "ID not found, cannot delete.",
"No_Results": "No Results",
"None": "None",
"NotFound": "Not found",
"NotFoundHelp": "The page or object you are looking for could not be found.",
"NotInShopping": "{food} is not in your shopping list.",
@@ -511,7 +514,7 @@
"Shopping_Categories": "Shopping Categories",
"Shopping_Category": "Shopping Category",
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
"Shopping_input_placeholder": "e.g. Potato/100 Potatoes/100 g Potatoes",
"Shopping_input_placeholder": "e.g. 100 g Potatoes",
"Shopping_list": "Shopping List",
"ShowDelayed": "Show delayed items",
"ShowIngredients": "Show Ingredients",
@@ -613,6 +616,8 @@
"Unrated": "Unrated",
"Up": "Up",
"Update": "Update",
"UpdateFoodLists": "Update Food Shoppinglists",
"UpdateFoodListsHelp": "Update the default shopping lists in the food when changing shopping lists during shopping.",
"Update_Existing_Data": "Update Existing Data",
"Updated": "Updated",
"UpgradeNow": "Upgrade now",

View File

@@ -36,6 +36,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineación",
"All": "",
"AllRecipes": "Todas las recetas",
"Amount": "Cantidad",
"App": "Aplicación",

View File

@@ -27,6 +27,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Tasaus",
"All": "",
"Amount": "Määrä",
"App": "Applikaatio",
"Apply": "",

View File

@@ -37,6 +37,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alignement",
"All": "",
"AllRecipes": "Toutes les recettes",
"Amount": "Quantité",
"App": "Appli",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "יישור",
"All": "",
"Amount": "כמות",
"App": "אפליקציה",
"Apply": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnanje",
"All": "",
"Amount": "Količina",
"App": "Aplikacija",
"Apply": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Igazítás",
"All": "",
"Amount": "Összeg",
"App": "Applikáció",
"Apply": "",

View File

@@ -17,6 +17,7 @@
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"All": "",
"Apply": "",
"Automate": "Ավտոմատացնել",
"BatchDeleteConfirm": "",

View File

@@ -25,6 +25,7 @@
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"All": "",
"App": "",
"Apply": "",
"Are_You_Sure": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"All": "",
"Amount": "",
"App": "",
"Apply": "",

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"All": "",
"AllRecipes": "",
"Amount": "",
"App": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"All": "",
"Amount": "Suma",
"App": "",
"Apply": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "",
"All": "",
"Amount": "",
"App": "",
"Apply": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"All": "",
"Amount": "Mengde",
"App": "App",
"Apply": "",

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Wyrównanie",
"All": "",
"AllRecipes": "Wszystkie przepisy",
"Amount": "Ilość",
"App": "Aplikacja",

View File

@@ -19,6 +19,7 @@
"Added_on": "Adicionado a",
"Advanced": "Avançado",
"Alignment": "Alinhamento",
"All": "",
"Amount": "Quantidade",
"Apply": "",
"Auto_Planner": "",

View File

@@ -36,6 +36,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alinhamento",
"All": "",
"AllRecipes": "Todas Receitas",
"Amount": "Quantidade",
"App": "Aplicação",

View File

@@ -26,6 +26,7 @@
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"All": "",
"Amount": "Cantitate",
"App": "Aplicație",
"Apply": "",

View File

@@ -37,6 +37,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Выравнивание",
"All": "",
"AllRecipes": "Все рецепты",
"Amount": "Количество",
"App": "Приложение",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Hizalama",
"All": "",
"Amount": "Miktar",
"App": "Uygulama",
"Apply": "",

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "校准",
"All": "",
"Amount": "数量",
"App": "应用",
"Apply": "",

View File

@@ -36,6 +36,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "對齊",
"All": "",
"AllRecipes": "所有食譜",
"Amount": "數量",
"App": "應用程式",

View File

@@ -31,6 +31,7 @@ models/FdcQueryFoods.ts
models/Food.ts
models/FoodBatchUpdate.ts
models/FoodInheritField.ts
models/FoodShopping.ts
models/FoodShoppingUpdate.ts
models/FoodSimple.ts
models/GenericModelReference.ts

View File

@@ -1972,7 +1972,7 @@ export interface ApiShoppingListCascadingListRequest {
}
export interface ApiShoppingListCreateRequest {
shoppingList?: Omit<ShoppingList, 'createdAt'|'updatedAt'>;
shoppingList?: ShoppingList;
}
export interface ApiShoppingListDestroyRequest {
@@ -2026,7 +2026,7 @@ export interface ApiShoppingListNullingListRequest {
export interface ApiShoppingListPartialUpdateRequest {
id: number;
patchedShoppingList?: Omit<PatchedShoppingList, 'createdAt'|'updatedAt'>;
patchedShoppingList?: PatchedShoppingList;
}
export interface ApiShoppingListProtectingListRequest {
@@ -2075,7 +2075,7 @@ export interface ApiShoppingListRetrieveRequest {
export interface ApiShoppingListUpdateRequest {
id: number;
shoppingList?: Omit<ShoppingList, 'createdAt'|'updatedAt'>;
shoppingList?: ShoppingList;
}
export interface ApiSpaceCreateRequest {

View File

@@ -13,6 +13,12 @@
*/
import { mapValues } from '../runtime';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { SupermarketCategory } from './SupermarketCategory';
import {
SupermarketCategoryFromJSON,
@@ -235,6 +241,12 @@ export interface Food {
* @memberof Food
*/
openDataSlug?: string;
/**
*
* @type {Array<ShoppingList>}
* @memberof Food
*/
shoppingLists?: Array<ShoppingList>;
}
/**
@@ -284,6 +296,7 @@ export function FoodFromJSONTyped(json: any, ignoreDiscriminator: boolean): Food
'substituteOnhand': json['substitute_onhand'],
'childInheritFields': json['child_inherit_fields'] == null ? undefined : ((json['child_inherit_fields'] as Array<any>).map(FoodInheritFieldFromJSON)),
'openDataSlug': json['open_data_slug'] == null ? undefined : json['open_data_slug'],
'shoppingLists': json['shopping_lists'] == null ? undefined : ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
};
}
@@ -312,6 +325,7 @@ export function FoodToJSON(value?: Omit<Food, 'shopping'|'parent'|'numchild'|'fu
'substitute_children': value['substituteChildren'],
'child_inherit_fields': value['childInheritFields'] == null ? undefined : ((value['childInheritFields'] as Array<any>).map(FoodInheritFieldToJSON)),
'open_data_slug': value['openDataSlug'],
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
};
}

View File

@@ -0,0 +1,106 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { mapValues } from '../runtime';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { SupermarketCategory } from './SupermarketCategory';
import {
SupermarketCategoryFromJSON,
SupermarketCategoryFromJSONTyped,
SupermarketCategoryToJSON,
} from './SupermarketCategory';
/**
*
* @export
* @interface FoodShopping
*/
export interface FoodShopping {
/**
*
* @type {number}
* @memberof FoodShopping
*/
id?: number;
/**
*
* @type {string}
* @memberof FoodShopping
*/
name: string;
/**
*
* @type {string}
* @memberof FoodShopping
*/
pluralName?: string;
/**
*
* @type {SupermarketCategory}
* @memberof FoodShopping
*/
readonly supermarketCategory: SupermarketCategory;
/**
*
* @type {Array<ShoppingList>}
* @memberof FoodShopping
*/
readonly shoppingLists: Array<ShoppingList>;
}
/**
* Check if a given object implements the FoodShopping interface.
*/
export function instanceOfFoodShopping(value: object): value is FoodShopping {
if (!('name' in value) || value['name'] === undefined) return false;
if (!('supermarketCategory' in value) || value['supermarketCategory'] === undefined) return false;
if (!('shoppingLists' in value) || value['shoppingLists'] === undefined) return false;
return true;
}
export function FoodShoppingFromJSON(json: any): FoodShopping {
return FoodShoppingFromJSONTyped(json, false);
}
export function FoodShoppingFromJSONTyped(json: any, ignoreDiscriminator: boolean): FoodShopping {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'],
'pluralName': json['plural_name'] == null ? undefined : json['plural_name'],
'supermarketCategory': SupermarketCategoryFromJSON(json['supermarket_category']),
'shoppingLists': ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
};
}
export function FoodShoppingToJSON(value?: Omit<FoodShopping, 'supermarketCategory'|'shoppingLists'> | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'plural_name': value['pluralName'],
};
}

View File

@@ -13,6 +13,12 @@
*/
import { mapValues } from '../runtime';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { SupermarketCategory } from './SupermarketCategory';
import {
SupermarketCategoryFromJSON,
@@ -235,6 +241,12 @@ export interface PatchedFood {
* @memberof PatchedFood
*/
openDataSlug?: string;
/**
*
* @type {Array<ShoppingList>}
* @memberof PatchedFood
*/
shoppingLists?: Array<ShoppingList>;
}
/**
@@ -278,6 +290,7 @@ export function PatchedFoodFromJSONTyped(json: any, ignoreDiscriminator: boolean
'substituteOnhand': json['substitute_onhand'] == null ? undefined : json['substitute_onhand'],
'childInheritFields': json['child_inherit_fields'] == null ? undefined : ((json['child_inherit_fields'] as Array<any>).map(FoodInheritFieldFromJSON)),
'openDataSlug': json['open_data_slug'] == null ? undefined : json['open_data_slug'],
'shoppingLists': json['shopping_lists'] == null ? undefined : ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
};
}
@@ -306,6 +319,7 @@ export function PatchedFoodToJSON(value?: Omit<PatchedFood, 'shopping'|'parent'|
'substitute_children': value['substituteChildren'],
'child_inherit_fields': value['childInheritFields'] == null ? undefined : ((value['childInheritFields'] as Array<any>).map(FoodInheritFieldToJSON)),
'open_data_slug': value['openDataSlug'],
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
};
}

View File

@@ -43,18 +43,6 @@ export interface PatchedShoppingList {
* @memberof PatchedShoppingList
*/
color?: string;
/**
*
* @type {Date}
* @memberof PatchedShoppingList
*/
readonly createdAt?: Date;
/**
*
* @type {Date}
* @memberof PatchedShoppingList
*/
readonly updatedAt?: Date;
}
/**
@@ -78,12 +66,10 @@ export function PatchedShoppingListFromJSONTyped(json: any, ignoreDiscriminator:
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'color': json['color'] == null ? undefined : json['color'],
'createdAt': json['created_at'] == null ? undefined : (new Date(json['created_at'])),
'updatedAt': json['updated_at'] == null ? undefined : (new Date(json['updated_at'])),
};
}
export function PatchedShoppingListToJSON(value?: Omit<PatchedShoppingList, 'createdAt'|'updatedAt'> | null): any {
export function PatchedShoppingListToJSON(value?: PatchedShoppingList | null): any {
if (value == null) {
return value;
}

View File

@@ -19,6 +19,18 @@ import {
UserFromJSONTyped,
UserToJSON,
} from './User';
import type { FoodShopping } from './FoodShopping';
import {
FoodShoppingFromJSON,
FoodShoppingFromJSONTyped,
FoodShoppingToJSON,
} from './FoodShopping';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { ShoppingListRecipe } from './ShoppingListRecipe';
import {
ShoppingListRecipeFromJSON,
@@ -31,12 +43,6 @@ import {
UnitFromJSONTyped,
UnitToJSON,
} from './Unit';
import type { Food } from './Food';
import {
FoodFromJSON,
FoodFromJSONTyped,
FoodToJSON,
} from './Food';
/**
* Adds nested create feature
@@ -58,10 +64,16 @@ export interface PatchedShoppingListEntry {
listRecipe?: number;
/**
*
* @type {Food}
* @type {Array<ShoppingList>}
* @memberof PatchedShoppingListEntry
*/
food?: Food;
shoppingLists?: Array<ShoppingList>;
/**
*
* @type {FoodShopping}
* @memberof PatchedShoppingListEntry
*/
food?: FoodShopping;
/**
*
* @type {Unit}
@@ -155,7 +167,8 @@ export function PatchedShoppingListEntryFromJSONTyped(json: any, ignoreDiscrimin
'id': json['id'] == null ? undefined : json['id'],
'listRecipe': json['list_recipe'] == null ? undefined : json['list_recipe'],
'food': json['food'] == null ? undefined : FoodFromJSON(json['food']),
'shoppingLists': json['shopping_lists'] == null ? undefined : ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
'food': json['food'] == null ? undefined : FoodShoppingFromJSON(json['food']),
'unit': json['unit'] == null ? undefined : UnitFromJSON(json['unit']),
'amount': json['amount'] == null ? undefined : json['amount'],
'order': json['order'] == null ? undefined : json['order'],
@@ -179,7 +192,8 @@ export function PatchedShoppingListEntryToJSON(value?: Omit<PatchedShoppingListE
'id': value['id'],
'list_recipe': value['listRecipe'],
'food': FoodToJSON(value['food']),
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
'food': FoodShoppingToJSON(value['food']),
'unit': UnitToJSON(value['unit']),
'amount': value['amount'],
'order': value['order'],

View File

@@ -62,18 +62,18 @@ export interface PatchedShoppingListRecipe {
* @memberof PatchedShoppingListRecipe
*/
readonly recipeData?: RecipeOverview;
/**
*
* @type {number}
* @memberof PatchedShoppingListRecipe
*/
mealplan?: number;
/**
*
* @type {MealPlan}
* @memberof PatchedShoppingListRecipe
*/
readonly mealPlanData?: MealPlan;
/**
*
* @type {number}
* @memberof PatchedShoppingListRecipe
*/
mealplan?: number;
/**
*
* @type {number}
@@ -109,8 +109,8 @@ export function PatchedShoppingListRecipeFromJSONTyped(json: any, ignoreDiscrimi
'name': json['name'] == null ? undefined : json['name'],
'recipe': json['recipe'] == null ? undefined : json['recipe'],
'recipeData': json['recipe_data'] == null ? undefined : RecipeOverviewFromJSON(json['recipe_data']),
'mealplan': json['mealplan'] == null ? undefined : json['mealplan'],
'mealPlanData': json['meal_plan_data'] == null ? undefined : MealPlanFromJSON(json['meal_plan_data']),
'mealplan': json['mealplan'] == null ? undefined : json['mealplan'],
'servings': json['servings'] == null ? undefined : json['servings'],
'createdBy': json['created_by'] == null ? undefined : UserFromJSON(json['created_by']),
};

View File

@@ -19,6 +19,12 @@ import {
SupermarketCategoryRelationFromJSONTyped,
SupermarketCategoryRelationToJSON,
} from './SupermarketCategoryRelation';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
/**
* Moves `UniqueValidator`'s from the validation stage to the save stage.
@@ -78,6 +84,12 @@ export interface PatchedSupermarket {
* @memberof PatchedSupermarket
*/
description?: string;
/**
*
* @type {Array<ShoppingList>}
* @memberof PatchedSupermarket
*/
shoppingLists?: Array<ShoppingList>;
/**
*
* @type {Array<SupermarketCategoryRelation>}
@@ -112,6 +124,7 @@ export function PatchedSupermarketFromJSONTyped(json: any, ignoreDiscriminator:
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'shoppingLists': json['shopping_lists'] == null ? undefined : ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
'categoryToSupermarket': json['category_to_supermarket'] == null ? undefined : ((json['category_to_supermarket'] as Array<any>).map(SupermarketCategoryRelationFromJSON)),
'openDataSlug': json['open_data_slug'] == null ? undefined : json['open_data_slug'],
};
@@ -126,6 +139,7 @@ export function PatchedSupermarketToJSON(value?: Omit<PatchedSupermarket, 'categ
'id': value['id'],
'name': value['name'],
'description': value['description'],
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
'open_data_slug': value['openDataSlug'],
};
}

View File

@@ -200,6 +200,12 @@ export interface PatchedUserPreference {
* @memberof PatchedUserPreference
*/
csvPrefix?: string;
/**
*
* @type {boolean}
* @memberof PatchedUserPreference
*/
shoppingUpdateFoodLists?: boolean;
/**
*
* @type {boolean}
@@ -273,6 +279,7 @@ export function PatchedUserPreferenceFromJSONTyped(json: any, ignoreDiscriminato
'shoppingRecentDays': json['shopping_recent_days'] == null ? undefined : json['shopping_recent_days'],
'csvDelim': json['csv_delim'] == null ? undefined : json['csv_delim'],
'csvPrefix': json['csv_prefix'] == null ? undefined : json['csv_prefix'],
'shoppingUpdateFoodLists': json['shopping_update_food_lists'] == null ? undefined : json['shopping_update_food_lists'],
'filterToSupermarket': json['filter_to_supermarket'] == null ? undefined : json['filter_to_supermarket'],
'shoppingAddOnhand': json['shopping_add_onhand'] == null ? undefined : json['shopping_add_onhand'],
'leftHanded': json['left_handed'] == null ? undefined : json['left_handed'],
@@ -309,6 +316,7 @@ export function PatchedUserPreferenceToJSON(value?: Omit<PatchedUserPreference,
'shopping_recent_days': value['shoppingRecentDays'],
'csv_delim': value['csvDelim'],
'csv_prefix': value['csvPrefix'],
'shopping_update_food_lists': value['shoppingUpdateFoodLists'],
'filter_to_supermarket': value['filterToSupermarket'],
'shopping_add_onhand': value['shoppingAddOnhand'],
'left_handed': value['leftHanded'],

View File

@@ -43,26 +43,12 @@ export interface ShoppingList {
* @memberof ShoppingList
*/
color?: string;
/**
*
* @type {Date}
* @memberof ShoppingList
*/
readonly createdAt: Date;
/**
*
* @type {Date}
* @memberof ShoppingList
*/
readonly updatedAt: Date;
}
/**
* Check if a given object implements the ShoppingList interface.
*/
export function instanceOfShoppingList(value: object): value is ShoppingList {
if (!('createdAt' in value) || value['createdAt'] === undefined) return false;
if (!('updatedAt' in value) || value['updatedAt'] === undefined) return false;
return true;
}
@@ -80,12 +66,10 @@ export function ShoppingListFromJSONTyped(json: any, ignoreDiscriminator: boolea
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'color': json['color'] == null ? undefined : json['color'],
'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])),
};
}
export function ShoppingListToJSON(value?: Omit<ShoppingList, 'createdAt'|'updatedAt'> | null): any {
export function ShoppingListToJSON(value?: ShoppingList | null): any {
if (value == null) {
return value;
}

View File

@@ -19,6 +19,18 @@ import {
UserFromJSONTyped,
UserToJSON,
} from './User';
import type { FoodShopping } from './FoodShopping';
import {
FoodShoppingFromJSON,
FoodShoppingFromJSONTyped,
FoodShoppingToJSON,
} from './FoodShopping';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { ShoppingListRecipe } from './ShoppingListRecipe';
import {
ShoppingListRecipeFromJSON,
@@ -31,12 +43,6 @@ import {
UnitFromJSONTyped,
UnitToJSON,
} from './Unit';
import type { Food } from './Food';
import {
FoodFromJSON,
FoodFromJSONTyped,
FoodToJSON,
} from './Food';
/**
* Adds nested create feature
@@ -58,10 +64,16 @@ export interface ShoppingListEntry {
listRecipe?: number;
/**
*
* @type {Food}
* @type {Array<ShoppingList>}
* @memberof ShoppingListEntry
*/
food: Food | null;
shoppingLists?: Array<ShoppingList>;
/**
*
* @type {FoodShopping}
* @memberof ShoppingListEntry
*/
food: FoodShopping | null;
/**
*
* @type {Unit}
@@ -161,7 +173,8 @@ export function ShoppingListEntryFromJSONTyped(json: any, ignoreDiscriminator: b
'id': json['id'] == null ? undefined : json['id'],
'listRecipe': json['list_recipe'] == null ? undefined : json['list_recipe'],
'food': FoodFromJSON(json['food']),
'shoppingLists': json['shopping_lists'] == null ? undefined : ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
'food': FoodShoppingFromJSON(json['food']),
'unit': json['unit'] == null ? undefined : UnitFromJSON(json['unit']),
'amount': json['amount'],
'order': json['order'] == null ? undefined : json['order'],
@@ -185,7 +198,8 @@ export function ShoppingListEntryToJSON(value?: Omit<ShoppingListEntry, 'listRec
'id': value['id'],
'list_recipe': value['listRecipe'],
'food': FoodToJSON(value['food']),
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
'food': FoodShoppingToJSON(value['food']),
'unit': UnitToJSON(value['unit']),
'amount': value['amount'],
'order': value['order'],

View File

@@ -62,18 +62,18 @@ export interface ShoppingListRecipe {
* @memberof ShoppingListRecipe
*/
readonly recipeData: RecipeOverview;
/**
*
* @type {number}
* @memberof ShoppingListRecipe
*/
mealplan?: number;
/**
*
* @type {MealPlan}
* @memberof ShoppingListRecipe
*/
readonly mealPlanData: MealPlan;
/**
*
* @type {number}
* @memberof ShoppingListRecipe
*/
mealplan?: number;
/**
*
* @type {number}
@@ -113,8 +113,8 @@ export function ShoppingListRecipeFromJSONTyped(json: any, ignoreDiscriminator:
'name': json['name'] == null ? undefined : json['name'],
'recipe': json['recipe'] == null ? undefined : json['recipe'],
'recipeData': RecipeOverviewFromJSON(json['recipe_data']),
'mealplan': json['mealplan'] == null ? undefined : json['mealplan'],
'mealPlanData': MealPlanFromJSON(json['meal_plan_data']),
'mealplan': json['mealplan'] == null ? undefined : json['mealplan'],
'servings': json['servings'],
'createdBy': UserFromJSON(json['created_by']),
};

View File

@@ -19,6 +19,12 @@ import {
SupermarketCategoryRelationFromJSONTyped,
SupermarketCategoryRelationToJSON,
} from './SupermarketCategoryRelation';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
/**
* Moves `UniqueValidator`'s from the validation stage to the save stage.
@@ -78,6 +84,12 @@ export interface Supermarket {
* @memberof Supermarket
*/
description?: string;
/**
*
* @type {Array<ShoppingList>}
* @memberof Supermarket
*/
shoppingLists?: Array<ShoppingList>;
/**
*
* @type {Array<SupermarketCategoryRelation>}
@@ -114,6 +126,7 @@ export function SupermarketFromJSONTyped(json: any, ignoreDiscriminator: boolean
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'],
'description': json['description'] == null ? undefined : json['description'],
'shoppingLists': json['shopping_lists'] == null ? undefined : ((json['shopping_lists'] as Array<any>).map(ShoppingListFromJSON)),
'categoryToSupermarket': ((json['category_to_supermarket'] as Array<any>).map(SupermarketCategoryRelationFromJSON)),
'openDataSlug': json['open_data_slug'] == null ? undefined : json['open_data_slug'],
};
@@ -128,6 +141,7 @@ export function SupermarketToJSON(value?: Omit<Supermarket, 'categoryToSupermark
'id': value['id'],
'name': value['name'],
'description': value['description'],
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
'open_data_slug': value['openDataSlug'],
};
}

View File

@@ -200,6 +200,12 @@ export interface UserPreference {
* @memberof UserPreference
*/
csvPrefix?: string;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
shoppingUpdateFoodLists?: boolean;
/**
*
* @type {boolean}
@@ -276,6 +282,7 @@ export function UserPreferenceFromJSONTyped(json: any, ignoreDiscriminator: bool
'shoppingRecentDays': json['shopping_recent_days'] == null ? undefined : json['shopping_recent_days'],
'csvDelim': json['csv_delim'] == null ? undefined : json['csv_delim'],
'csvPrefix': json['csv_prefix'] == null ? undefined : json['csv_prefix'],
'shoppingUpdateFoodLists': json['shopping_update_food_lists'] == null ? undefined : json['shopping_update_food_lists'],
'filterToSupermarket': json['filter_to_supermarket'] == null ? undefined : json['filter_to_supermarket'],
'shoppingAddOnhand': json['shopping_add_onhand'] == null ? undefined : json['shopping_add_onhand'],
'leftHanded': json['left_handed'] == null ? undefined : json['left_handed'],
@@ -312,6 +319,7 @@ export function UserPreferenceToJSON(value?: Omit<UserPreference, 'user'|'foodIn
'shopping_recent_days': value['shoppingRecentDays'],
'csv_delim': value['csvDelim'],
'csv_prefix': value['csvPrefix'],
'shopping_update_food_lists': value['shoppingUpdateFoodLists'],
'filter_to_supermarket': value['filterToSupermarket'],
'shopping_add_onhand': value['shoppingAddOnhand'],
'left_handed': value['leftHanded'],

View File

@@ -29,6 +29,7 @@ export * from './FdcQueryFoods';
export * from './Food';
export * from './FoodBatchUpdate';
export * from './FoodInheritField';
export * from './FoodShopping';
export * from './FoodShoppingUpdate';
export * from './FoodSimple';
export * from './GenericModelReference';

View File

@@ -1,6 +1,6 @@
import {acceptHMRUpdate, defineStore} from "pinia"
import {ApiApi, ApiShoppingListEntryListRequest, Food, Recipe, ShoppingListEntry, ShoppingListEntryBulk, ShoppingListRecipe, Supermarket, SupermarketCategory} from "@/openapi";
import {computed, ref} from "vue";
import {computed, ref, shallowRef, triggerRef} from "vue";
import {
IShoppingExportEntry,
IShoppingList,
@@ -25,14 +25,6 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let supermarketCategories = ref([] as SupermarketCategory[])
let supermarkets = ref([] as Supermarket[])
let stats = ref({
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats)
// internal
let currentlyUpdating = ref(false)
let initialized = ref(false)
@@ -41,28 +33,25 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let autoSyncHasFocus = ref(true)
let autoSyncTimeoutId = ref(0)
let undoStack = ref([] as ShoppingOperationHistoryEntry[])
let undoStack = shallowRef([] as ShoppingOperationHistoryEntry[])
let queueTimeoutId = ref(-1)
let itemCheckSyncQueue = ref([] as IShoppingSyncQueueEntry[])
let itemCheckSyncQueue = shallowRef([] as IShoppingSyncQueueEntry[])
let syncQueueRunning = ref(false)
let entriesByGroup = shallowRef([] as IShoppingListCategory[])
/**
* build a multi-level data structure ready for display from shopping list entries
* group by selected grouping key
*/
const getEntriesByGroup = computed(() => {
stats.value = {
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats
function updateEntriesStructure() {
let structure = {} as IShoppingList
structure.categories = new Map<string, IShoppingListCategory>
if (useUserPreferenceStore().deviceSettings.shopping_selected_grouping === ShoppingGroupingOptions.CATEGORY && useUserPreferenceStore().deviceSettings.shopping_selected_supermarket != null) {
useUserPreferenceStore().deviceSettings.shopping_selected_supermarket.categoryToSupermarket.forEach(cTS => {
const deviceSettings = useUserPreferenceStore().deviceSettings
if (deviceSettings.shopping_selected_grouping === ShoppingGroupingOptions.CATEGORY && deviceSettings.shopping_selected_supermarket != null) {
deviceSettings.shopping_selected_supermarket.categoryToSupermarket.forEach(cTS => {
structure.categories.set(cTS.category.name, {'name': cTS.category.name, 'foods': new Map<number, IShoppingListFood>} as IShoppingListCategory)
})
}
@@ -71,46 +60,9 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
// build structure
entries.value.forEach(shoppingListEntry => {
structure = updateEntryInStructure(structure, shoppingListEntry)
})
// statistics for UI conditions and display
structure.categories.forEach(category => {
let categoryStats = {
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats
category.foods.forEach(food => {
let food_checked = true
food.entries.forEach(entry => {
if (entry.checked) {
categoryStats.countChecked++
} else {
if (isDelayed(entry)) {
categoryStats.countUncheckedDelayed++
} else {
categoryStats.countUnchecked++
}
}
})
if (food_checked) {
categoryStats.countCheckedFood++
} else {
categoryStats.countUncheckedFood++
}
})
category.stats = categoryStats
stats.value.countChecked += categoryStats.countChecked
stats.value.countUnchecked += categoryStats.countUnchecked
stats.value.countCheckedFood += categoryStats.countCheckedFood
stats.value.countUncheckedFood += categoryStats.countUncheckedFood
if (isEntryVisible(shoppingListEntry, deviceSettings)) {
structure = updateEntryInStructure(structure, shoppingListEntry)
}
})
// ordering
@@ -121,10 +73,27 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
}
structure.categories.forEach(category => {
orderedStructure.push(category)
if (category.foods.size > 0) {
orderedStructure.push(category)
}
})
return orderedStructure
entriesByGroup.value = orderedStructure
}
/**
* get the total number of foods in the shopping list
* since entries are always grouped by food, it makes no sense to display the entry count anywhere
*/
let totalFoods = computed(() => {
let count = 0
if (initialized.value) {
entriesByGroup.value.forEach(category => {
count += category.foods.size
})
}
return count
})
/**
@@ -135,7 +104,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
function getFlatEntries() {
let items: IShoppingExportEntry[] = []
getEntriesByGroup.value.forEach(shoppingListEntry => {
entriesByGroup.value.forEach(shoppingListEntry => {
shoppingListEntry.foods.forEach(food => {
food.entries.forEach(entry => {
items.push({
@@ -175,7 +144,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
function hasFailedItems() {
for (let i in itemCheckSyncQueue.value) {
if (itemCheckSyncQueue.value[i]['status'] === 'syncing_failed_before' || itemCheckSyncQueue.value[i]['status'] === 'waiting_failed_before') {
return true
return !syncQueueRunning.value
}
}
return false
@@ -197,6 +166,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
} else {
// only clear local entries when not given a meal plan to not accidentally filter the shopping list
entries.value = new Map<number, ShoppingListEntry>
initialized.value = false
}
recLoadShoppingListEntries(requestParameters)
@@ -221,17 +191,30 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
*/
function recLoadShoppingListEntries(requestParameters: ApiShoppingListEntryListRequest) {
let api = new ApiApi()
api.apiShoppingListEntryList(requestParameters).then((r) => {
return api.apiShoppingListEntryList(requestParameters).then((r) => {
let promises = [] as Promise<any>[]
let newMap = new Map<number, ShoppingListEntry>()
r.results.forEach((e) => {
entries.value.set(e.id!, e)
newMap.set(e.id!, e)
})
if (r.next) {
requestParameters.page = requestParameters.page + 1
recLoadShoppingListEntries(requestParameters)
} else {
currentlyUpdating.value = false
initialized.value = true
// bulk assign to avoid unnecessary reactivity updates
entries.value = new Map([...entries.value, ...newMap])
if (requestParameters.page == 1) {
if (r.next) {
while (Math.ceil(r.count / requestParameters.pageSize) > requestParameters.page) {
requestParameters.page = requestParameters.page + 1
promises.push(recLoadShoppingListEntries(requestParameters))
}
}
Promise.allSettled(promises).then(() => {
updateEntriesStructure()
currentlyUpdating.value = false
initialized.value = true
})
}
}).catch((err) => {
currentlyUpdating.value = false
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -251,6 +234,9 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
r.results.forEach((e) => {
entries.value.set(e.id!, e)
})
if(r.results.length > 0){
updateEntriesStructure()
}
currentlyUpdating.value = false
}).catch((err: any) => {
currentlyUpdating.value = false
@@ -267,6 +253,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
const api = new ApiApi()
return api.apiShoppingListEntryCreate({shoppingListEntry: object}).then((r) => {
entries.value.set(r.id!, r)
updateEntriesStructure()
if (undo) {
registerChange("CREATE", [r])
}
@@ -299,6 +286,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
})
}
/**
* delete shopping list entry object from DB and store
* @param object entry object to delete
@@ -308,6 +296,19 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
const api = new ApiApi()
return api.apiShoppingListEntryDestroy({id: object.id!}).then((r) => {
entries.value.delete(object.id!)
let categoryName = getEntryCategoryKey(object)
entriesByGroup.value.forEach(category => {
if (category.name == categoryName) {
category.foods.get(object.food!.id!)?.entries.delete(object.id!)
if(category.foods.get(object.food!.id!)?.entries.size == 0) {
category.foods.delete(object.food!.id!)
triggerRef(entriesByGroup)
}
}
})
if (undo) {
registerChange("DESTROY", [object])
}
@@ -331,7 +332,32 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
return recipes
}
// convenience methods
/**
* get the key (name) of the IShoppingListCategory that an entry belongs to
* @param object
*/
function getEntryCategoryKey(object: ShoppingListEntry) {
let group = useUserPreferenceStore().deviceSettings.shopping_selected_grouping
let groupingKey = UNDEFINED_CATEGORY
if (group == ShoppingGroupingOptions.CATEGORY && object.food != null && object.food.supermarketCategory != null) {
groupingKey = object.food?.supermarketCategory?.name
} else if (group == ShoppingGroupingOptions.CREATED_BY) {
groupingKey = object.createdBy.displayName
} else if (group == ShoppingGroupingOptions.RECIPE && object.listRecipeData != null) {
if (object.listRecipeData.recipeData != null) {
groupingKey = object.listRecipeData.recipeData.name
if (object.listRecipeData.mealPlanData != null) {
groupingKey += ' - ' + object.listRecipeData.mealPlanData.mealType.name + ' - ' + DateTime.fromJSDate(object.listRecipeData.mealPlanData.fromDate).toLocaleString(DateTime.DATE_SHORT)
}
}
}
return groupingKey
}
/**
* puts an entry into the appropriate group of the IShoppingList datastructure
* if a group does not yet exist and the sorting is not set to category with selected supermarket only, it will be created
@@ -339,23 +365,8 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
* @param entry
*/
function updateEntryInStructure(structure: IShoppingList, entry: ShoppingListEntry) {
let groupingKey = UNDEFINED_CATEGORY
let group = useUserPreferenceStore().deviceSettings.shopping_selected_grouping
if (group == ShoppingGroupingOptions.CATEGORY && entry.food != null && entry.food.supermarketCategory != null) {
groupingKey = entry.food?.supermarketCategory?.name
} else if (group == ShoppingGroupingOptions.CREATED_BY) {
groupingKey = entry.createdBy.displayName
} else if (group == ShoppingGroupingOptions.RECIPE && entry.listRecipeData != null) {
if (entry.listRecipeData.recipeData != null) {
groupingKey = entry.listRecipeData.recipeData.name
if (entry.listRecipeData.mealPlanData != null) {
groupingKey += ' - ' + entry.listRecipeData.mealPlanData.mealType.name + ' - ' + DateTime.fromJSDate(entry.listRecipeData.mealPlanData.fromDate).toLocaleString(DateTime.DATE_SHORT)
}
}
}
let groupingKey = getEntryCategoryKey(entry)
if (!structure.categories.has(groupingKey) && !(group == ShoppingGroupingOptions.CATEGORY && useUserPreferenceStore().deviceSettings.shopping_show_selected_supermarket_only)) {
structure.categories.set(groupingKey, {'name': groupingKey, 'foods': new Map<number, IShoppingListFood>} as IShoppingListCategory)
@@ -383,19 +394,18 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
if (undo) {
registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
}
let entryIdList: number[] = []
entries.forEach(entry => {
entry.checked = checked
entryIdList.push(entry.id!)
})
itemCheckSyncQueue.value.push({
ids: entryIdList,
checked: checked,
status: 'waiting',
} as IShoppingSyncQueueEntry)
runSyncQueue(5)
runSyncQueue(100)
}
/**
@@ -409,15 +419,17 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let api = new ApiApi()
let promises: Promise<void>[] = []
let updatedEntries = new Map<number, ShoppingListEntry>()
itemCheckSyncQueue.value.forEach((entry, index) => {
entry['status'] = ((entry['status'] === 'waiting') ? 'syncing' : 'syncing_failed_before')
entry['status'] = ((entry['status'] === 'waiting_failed_before') ? 'syncing_failed_before' : 'syncing')
syncQueueRunning.value = true
let p = api.apiShoppingListEntryBulkCreate({shoppingListEntryBulk: entry}, {}).then((r) => {
entry.ids.forEach(id => {
let e = entries.value.get(id)
e.updatedAt = r.timestamp
e.checked = r.checked
entries.value.set(id, e)
if (e) {
e.updatedAt = r.timestamp
updatedEntries.set(id, e)
}
})
itemCheckSyncQueue.value.splice(index, 1)
}).catch((err) => {
@@ -432,6 +444,10 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
})
Promise.allSettled(promises).finally(() => {
entries.value = new Map([...entries.value, ...updatedEntries])
syncQueueRunning.value = false
//TODO proper function to splice/update structure as needed
useShoppingStore().updateEntriesStructure()
if (itemCheckSyncQueue.value.length > 0) {
runSyncQueue(500)
}
@@ -579,7 +595,8 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
entries,
supermarkets,
supermarketCategories,
getEntriesByGroup,
updateEntriesStructure,
entriesByGroup,
autoSyncTimeoutId,
autoSyncHasFocus,
autoSyncLastTimestamp,
@@ -589,7 +606,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
hasFailedItems,
itemCheckSyncQueue,
undoStack,
stats,
totalFoods,
refreshFromAPI,
autoSync,
createObject,

View File

@@ -228,6 +228,7 @@ export const useUserPreferenceStore = defineStore('user_preference_store', () =>
shopping_show_selected_supermarket_only: false,
shopping_selected_grouping: ShoppingGroupingOptions.CATEGORY,
shopping_selected_supermarket: null,
shopping_selected_shopping_list: [],
shopping_item_info_created_by: false,
shopping_item_info_mealplan: true,
shopping_item_info_recipe: true,

View File

@@ -23,7 +23,6 @@ export interface IShoppingList {
export interface IShoppingListCategory {
name: string,
foods: Map<number, IShoppingListFood>,
stats: ShoppingListStats,
}
/**

View File

@@ -1,4 +1,4 @@
import {Supermarket} from "@/openapi";
import {ShoppingList, Supermarket} from "@/openapi";
export type DeviceSettings = {
shopping_show_checked_entries: boolean
@@ -6,6 +6,7 @@ export type DeviceSettings = {
shopping_show_selected_supermarket_only: boolean
shopping_selected_grouping: string
shopping_selected_supermarket: Supermarket | null
shopping_selected_shopping_list: number[]
shopping_item_info_created_by: boolean
shopping_item_info_mealplan: boolean
shopping_item_info_recipe: boolean

View File

@@ -18,6 +18,15 @@ export function isEntryVisible(entry: ShoppingListEntry, deviceSettings: DeviceS
if (entry.checked && !deviceSettings.shopping_show_checked_entries) {
entryVisible = false
}
// if no list is selected show all entries
// if -1 is selected show entries without shopping lists
// otherwise check if at least one of the entries lists is selected
if(deviceSettings.shopping_selected_shopping_list.length > 0){
if(!(deviceSettings.shopping_selected_shopping_list.includes(-1) && entry.shoppingLists?.length == 0) && !deviceSettings.shopping_selected_shopping_list.some(sl => (entry.shoppingLists?.findIndex(eSl => eSl.id == sl) != -1))){
entryVisible = false
}
}
return entryVisible
}
@@ -60,16 +69,15 @@ export function isShoppingListFoodDelayed(slf: IShoppingListFood) {
* @param category
*/
export function isShoppingCategoryVisible(category: IShoppingListCategory) {
let entryCount = category.stats.countUnchecked
console.log('checking if category is visible')
let categoryVisible = false
category.foods.forEach(food => {
if(isShoppingListFoodVisible(food, useUserPreferenceStore().deviceSettings)){
categoryVisible = true
}
})
if (useUserPreferenceStore().deviceSettings.shopping_show_checked_entries) {
entryCount += category.stats.countChecked
}
if (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries) {
entryCount += category.stats.countUncheckedDelayed
}
return entryCount > 0
return categoryVisible
}
// -------------- SPACE RELATED ----------------------