Compare commits

...

47 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
vabene1111
f4eded5b03 basics for multiple shopping lists 2025-11-30 14:57:14 +01:00
Vincenzo Reale
3d9c51053a Translated using Weblate (Italian)
Currently translated at 100.0% (871 of 871 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/it/
2025-11-29 10:31:05 +00:00
vabene1111
0046233b6f open print with servings 2025-11-24 20:39:53 +01:00
vabene1111
78ede7b601 Merge pull request #4237 from Orycterope/fix_servings
open meal plan recipe with right amount of servings
2025-11-24 20:29:15 +01:00
vabene1111
7e7e133604 Merge branch 'develop' into fix_servings 2025-11-24 20:28:22 +01:00
vabene1111
b0ec569a00 fixed meal master importer 2025-11-24 20:24:44 +01:00
vabene1111
7674084ae0 potentially fixed redirect issue 2025-11-24 20:14:37 +01:00
vabene1111
798e2ac48b fixed mealie import 2025-11-24 20:05:58 +01:00
vabene1111
714d4a32a9 Merge branch 'weblate-develop' into develop
# Conflicts:
#	vue3/src/locales/sv.json
2025-11-24 19:51:27 +01:00
Andreas Ljungberg
66b5097872 Translated using Weblate (Swedish)
Currently translated at 69.8% (607 of 869 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/sv/
2025-11-22 20:03:33 +00:00
SerhiiOS
c1d4fed142 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-11-22 20:03:33 +00:00
vabene1111
1cc0806729 remove empty default ingredient when using paste improt 2025-11-19 21:50:47 +01:00
vabene1111
09487a0e94 improved ical export 2025-11-19 21:47:23 +01:00
vabene1111
89e58edcad fixed SLR's without entries 2025-11-19 21:30:20 +01:00
vabene1111
887d7fe9f0 no more unaccent on sqlite 2025-11-19 21:18:23 +01:00
vabene1111
14696e3ce8 fixed times cooked in saved serarch and improved frontend query binding 2025-11-19 21:02:16 +01:00
vabene1111
dd56bb4b35 fixed property detail dialog serving scaling 2025-11-19 20:17:39 +01:00
vabene1111
8ec0ba9541 fixed help view not navigatbale on mobile 2025-11-19 20:06:39 +01:00
vabene1111
c105c9190e ignore defunct websites in url list import mode 2025-11-18 16:16:39 +01:00
vabene1111
9c1700adb9 fixed copying recipes would link data 2025-11-18 16:07:36 +01:00
vabene1111
b43a87a7e3 improved mealie importer 2025-11-18 15:59:15 +01:00
vabene1111
01e78baecf fixed property helper error 2025-11-18 15:48:39 +01:00
vabene1111
0ee241524d some more print mode tweaks 2025-11-18 15:42:19 +01:00
orycterope
e0196f17da open meal plan recipe with given servings
Fix https://github.com/TandoorRecipes/recipes/issues/3787

Adds a '?servings=42' query param to RecipeViewPage, and propagates it
to child views via a prop. The ingredientFactor is computed based on
this param if it is present, and defaults to recipe servings otherwise.

This query param is set when comming from:

* Home page's HorizontalRecipeWindow
* MealPlanEditor

This commit also makes the RecipeView's Activity form reactive on
the number of servings, before creating a comment.
2025-11-18 14:10:52 +01:00
99 changed files with 9178 additions and 7477 deletions

View File

@@ -56,7 +56,7 @@ class FoodPropertyHelper:
if p.property_type == pt and p.property_amount is not None: if p.property_type == pt and p.property_amount is not None:
has_property_value = True has_property_value = True
for c in conversions: for c in conversions:
if c.unit == i.food.properties_food_unit: if c.unit == i.food.properties_food_unit and i.food.properties_food_amount != 0:
found_property = True found_property = True
computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount computed_properties[pt.id]['total_value'] += (c.amount / i.food.properties_food_amount) * p.property_amount
computed_properties[pt.id]['food_values'] = self.add_or_create( computed_properties[pt.id]['food_values'] = self.add_or_create(

View File

@@ -324,9 +324,9 @@ class RecipeSearch():
self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0))) self._queryset = self._queryset.annotate(recent=Coalesce(Max(Case(When(pk__in=num_recent_recipes.values('recipe'), then='viewlog__pk'))), Value(0)))
def _favorite_recipes(self): def _favorite_recipes(self):
if self._sort_includes('favorite') or self._timescooked or self._timescooked_gte or self._timescooked_lte: if self._sort_includes('favorite') or self._timescooked is not None or self._timescooked_gte is not None or self._timescooked_lte is not None:
less_than = self._timescooked_lte and not self._sort_includes('-favorite') less_than = self._timescooked_lte and not self._sort_includes('-favorite')
if less_than or self._timescooked == 0: if less_than:
default = 1000 default = 1000
else: else:
default = 0 default = 0
@@ -338,11 +338,11 @@ class RecipeSearch():
) )
self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default)) self._queryset = self._queryset.annotate(favorite=Coalesce(Subquery(favorite_recipes), default))
if self._timescooked: if self._timescooked is not None:
self._queryset = self._queryset.filter(favorite=self._timescooked) self._queryset = self._queryset.filter(favorite=self._timescooked)
elif self._timescooked_lte: elif self._timescooked_lte is not None:
self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0) self._queryset = self._queryset.filter(favorite__lte=int(self._timescooked_lte)).exclude(favorite=0)
elif self._timescooked_gte: elif self._timescooked_gte is not None:
self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte)) self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte))
def keyword_filters(self, **kwargs): def keyword_filters(self, **kwargs):

View File

@@ -75,7 +75,8 @@ class RecipeShoppingEditor():
@staticmethod @staticmethod
def get_shopping_list_recipe(id, user, space): def get_shopping_list_recipe(id, user, space):
return ShoppingListRecipe.objects.filter(id=id).filter(entries__space=space).filter( # TODO this sucks since it wont find SLR's that no longer have any entries
return ShoppingListRecipe.objects.filter(id=id, space=space).filter(
Q(entries__created_by=user) Q(entries__created_by=user)
| Q(entries__created_by__in=list(user.get_shopping_share())) | Q(entries__created_by__in=list(user.get_shopping_share()))
).prefetch_related('entries').first() ).prefetch_related('entries').first()
@@ -136,7 +137,8 @@ class RecipeShoppingEditor():
self.servings = servings self.servings = servings
self._delete_ingredients(ingredients=ingredients) self._delete_ingredients(ingredients=ingredients)
if self.servings != self._shopping_list_recipe.servings: # need to check if there is a SLR because its possible it cant be found if all entries are deleted
if self._shopping_list_recipe and self.servings != self._shopping_list_recipe.servings:
self.edit_servings() self.edit_servings()
self._add_ingredients(ingredients=ingredients) self._add_ingredients(ingredients=ingredients)
return True return True
@@ -175,8 +177,9 @@ class RecipeShoppingEditor():
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True) existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing) add_ingredients = ingredients.exclude(id__in=existing)
entries = []
for i in [x for x in add_ingredients if x.food]: for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create( entry = ShoppingListEntry(
list_recipe=self._shopping_list_recipe, list_recipe=self._shopping_list_recipe,
food=i.food, food=i.food,
unit=i.unit, unit=i.unit,
@@ -185,6 +188,12 @@ class RecipeShoppingEditor():
created_by=self.created_by, created_by=self.created_by,
space=self.space, 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 # deletes shopping list entries not in ingredients list
def _delete_ingredients(self, ingredients=None): def _delete_ingredients(self, ingredients=None):

View File

@@ -96,14 +96,20 @@ class Mealie1(Integration):
self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n" self.import_log.msg += f"Ignoring {r['name']} because a recipe with this name already exists.\n"
self.import_log.save() self.import_log.save()
else: else:
servings = 1
try:
servings = r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1
except KeyError:
pass
recipe = Recipe.objects.create( recipe = Recipe.objects.create(
waiting_time=parse_time(r['perform_time']), waiting_time=parse_time(r['perform_time']),
working_time=parse_time(r['prep_time']), working_time=parse_time(r['prep_time']),
description=r['description'][:512], description=r['description'][:512],
name=r['name'], name=r['name'],
source_url=r['org_url'], source_url=r['org_url'],
servings=r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1, servings=servings,
servings_text=r['recipe_yield'].strip() if r['recipe_yield'] else "", servings_text=r['recipe_yield'].strip()[:32] if r['recipe_yield'] else "",
internal=True, internal=True,
created_at=r['created_at'], created_at=r['created_at'],
space=self.request.space, space=self.request.space,
@@ -131,7 +137,7 @@ class Mealie1(Integration):
step_id_dict = {} step_id_dict = {}
for s in mealie_database['recipe_instructions']: for s in mealie_database['recipe_instructions']:
if s['recipe_id'] in recipes_dict: if s['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if s['summary'] else ""), step = Step.objects.create(instruction=(s['text'] if s['text'] else "") + (f" \n {s['summary']}" if 'summary' in s and s['summary'] else ""),
order=s['position'], order=s['position'],
name=s['title'], name=s['title'],
space=self.request.space) space=self.request.space)
@@ -153,7 +159,7 @@ class Mealie1(Integration):
for n in mealie_database['notes']: for n in mealie_database['notes']:
if n['recipe_id'] in recipes_dict: if n['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=n['text'], step = Step.objects.create(instruction=n['text'],
name=n['title'], name=n['title'][:128] if n['title'] else "",
order=100, order=100,
space=self.request.space) space=self.request.space)
steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[n['recipe_id']], step_id=step.pk)) steps_relation.append(Recipe.steps.through(recipe_id=recipes_dict[n['recipe_id']], step_id=step.pk))
@@ -191,7 +197,7 @@ class Mealie1(Integration):
space=self.request.space, 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)) 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()) amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
f = ingredient_parser.get_food(food) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
@@ -243,7 +249,7 @@ class Mealie1(Integration):
for r in mealie_database['recipe_nutrition']: for r in mealie_database['recipe_nutrition']:
if r['recipe_id'] in recipes_dict: if r['recipe_id'] in recipes_dict:
for key in property_types_dict: for key in property_types_dict:
if r[key]: if key in r and r[key]:
properties_relation.append( properties_relation.append(
Property(property_type_id=property_types_dict[key].pk, Property(property_type_id=property_types_dict[key].pk,
property_amount=Decimal(str(r[key])) / ( property_amount=Decimal(str(r[key])) / (

View File

@@ -63,7 +63,15 @@ class MealMaster(Integration):
current_recipe = '' current_recipe = ''
for fl in file.readlines(): for fl in file.readlines():
line = fl.decode("windows-1250") line = ""
try:
line = fl.decode("UTF-8")
except UnicodeDecodeError:
try:
line = fl.decode("windows-1250")
except Exception as e:
line = "ERROR DECODING LINE"
if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower(): if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower():
if current_recipe != '': if current_recipe != '':
recipe_list.append(current_recipe) recipe_list.append(current_recipe)

View File

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

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-01 15:04+0200\n" "POT-Creation-Date: 2024-08-01 15:04+0200\n"
"PO-Revision-Date: 2025-11-18 07:01+0000\n" "PO-Revision-Date: 2025-12-02 08:03+0000\n"
"Last-Translator: SerhiiOS <serhios@users.noreply.translate.tandoor.dev>\n" "Last-Translator: SerhiiOS <serhios@users.noreply.translate.tandoor.dev>\n"
"Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/" "Language-Team: Ukrainian <http://translate.tandoor.dev/projects/tandoor/"
"recipes-backend/uk/>\n" "recipes-backend/uk/>\n"
@@ -468,7 +468,7 @@ msgstr "Пошук"
#: .\cookbook\models.py:455 .\cookbook\templates\base.html:114 #: .\cookbook\models.py:455 .\cookbook\templates\base.html:114
#: .\cookbook\templates\meal_plan.html:7 #: .\cookbook\templates\meal_plan.html:7
msgid "Meal-Plan" msgid "Meal-Plan"
msgstr "План харчування" msgstr "Меню"
#: .\cookbook\models.py:456 .\cookbook\templates\base.html:122 #: .\cookbook\models.py:456 .\cookbook\templates\base.html:122
#: .\cookbook\views\views.py:459 #: .\cookbook\views\views.py:459
@@ -1085,7 +1085,7 @@ msgstr "Ви використовуєте безкоштовну версію Ta
#: .\cookbook\templates\base.html:407 #: .\cookbook\templates\base.html:407
msgid "Upgrade Now" msgid "Upgrade Now"
msgstr "Оновити зараз" msgstr "Оновити Зараз"
#: .\cookbook\templates\batch\edit.html:6 #: .\cookbook\templates\batch\edit.html:6
msgid "Batch edit Category" msgid "Batch edit Category"
@@ -1447,7 +1447,7 @@ msgstr "Таблиця"
#: .\cookbook\templates\markdown_info.html:155 #: .\cookbook\templates\markdown_info.html:155
#: .\cookbook\templates\markdown_info.html:172 #: .\cookbook\templates\markdown_info.html:172
msgid "Header" msgid "Header"
msgstr "Заголовок" msgstr "Шапка"
#: .\cookbook\templates\markdown_info.html:157 #: .\cookbook\templates\markdown_info.html:157
#: .\cookbook\templates\markdown_info.html:178 #: .\cookbook\templates\markdown_info.html:178
@@ -2639,7 +2639,7 @@ msgstr "Конфігурація коннектора для бекенду"
#: .\cookbook\views\lists.py:91 #: .\cookbook\views\lists.py:91
msgid "Invite Links" msgid "Invite Links"
msgstr "Посилання для запрошення" msgstr "Посилання для запрошеннь"
#: .\cookbook\views\lists.py:154 #: .\cookbook\views\lists.py:154
msgid "Supermarkets" msgid "Supermarkets"
@@ -2790,7 +2790,7 @@ msgstr ""
#: .\cookbook\views\views.py:451 #: .\cookbook\views\views.py:451
msgid "Manage recipes, shopping list, meal plans and more." msgid "Manage recipes, shopping list, meal plans and more."
msgstr "Керуйте рецептами, списком покупок, планами харчування тощо." msgstr "Керуйте рецептами, списком покупок, меню тощо."
#: .\cookbook\views\views.py:458 #: .\cookbook\views\views.py:458
msgid "Plan" msgid "Plan"

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.7 on 2025-11-30 10:19
import cookbook.models
import django.db.models.deletion
import django_prometheus.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0231_alter_aiprovider_options_alter_automation_options_and_more'),
]
operations = [
migrations.CreateModel(
name='ShoppingList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, default='', max_length=32)),
('description', models.TextField(blank=True)),
('color', models.CharField(blank=True, max_length=7, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(django_prometheus.models.ExportModelOperationsMixin('shopping_list'), models.Model, cookbook.models.PermissionModelMixin),
),
]

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) show_step_ingredients = models.BooleanField(default=True)
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4) default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
shopping_recent_days = models.PositiveIntegerField(default=7) shopping_recent_days = models.PositiveIntegerField(default=7)
shopping_update_food_lists = models.BooleanField(default=True)
csv_delim = models.CharField(max_length=2, default=",") csv_delim = models.CharField(max_length=2, default=",")
csv_prefix = models.CharField(max_length=10, blank=True, ) 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)]) name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation') 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) open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE) 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) recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
url = models.CharField(max_length=1024, blank=True, null=True, default='') 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 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 ignore_shopping = models.BooleanField(default=False) # inherited field
onhand_users = models.ManyToManyField(User, blank=True) onhand_users = models.ManyToManyField(User, blank=True)
description = models.TextField(default='', 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) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
def __str__(self): # def __str__(self):
return f'{self.pk}: {self.amount} ' + (self.food.name if self.food else ' ') + (self.unit.name if self.unit else '') # return f'{self.pk}: {self.amount} ' + (self.food.name if self.food else ' ') + (self.unit.name if self.unit else '')
class Meta: class Meta:
ordering = ['order', 'pk'] ordering = ['order', 'pk']
@@ -1157,7 +1160,7 @@ class Comment(ExportModelOperationsMixin('comment'), models.Model, PermissionMod
def __str__(self): def __str__(self):
return self.text return self.text
class Meta: class Meta:
ordering = ('pk',) ordering = ('pk',)
@@ -1297,14 +1300,30 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
def __str__(self): # def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}' # return f'Shopping list recipe {self.id} - {self.recipe}'
class Meta:
ordering = ('pk',)
class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=32, blank=True, default='')
description = models.TextField(blank=True)
color = models.CharField(max_length=7, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
class Meta: class Meta:
ordering = ('pk',) ordering = ('pk',)
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin): 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') 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') 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) unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)

View File

@@ -1,3 +1,4 @@
import traceback
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
@@ -37,7 +38,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
ShareLink, ShoppingListEntry, ShoppingListRecipe, Space, ShareLink, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider) UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig, SearchPreference, SearchFields, AiLog, AiProvider, ShoppingList)
from cookbook.templatetags.custom_tags import markdown from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST from recipes.settings import AWS_ENABLED, MEDIA_URL, EMAIL_HOST
@@ -186,11 +187,37 @@ class SpaceFilterSerializer(serializers.ListSerializer):
if isinstance(self.context['request'].user, AnonymousUser): if isinstance(self.context['request'].user, AnonymousUser):
data = [] data = []
else: 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): 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] data = [d for d in data if getattr(d, self.child.Meta.model.get_space_key()[0]) == self.context['request'].space]
else: 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) return super().to_representation(data)
@@ -484,6 +511,20 @@ class SpacedModelSerializer(serializers.ModelSerializer):
return super().create(validated_data) 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): class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data): def create(self, validated_data):
@@ -533,7 +574,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
'food_inherit_default', 'default_delay', 'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', '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', 'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'show_step_ingredients',
'food_children_exist' 'food_children_exist'
) )
@@ -648,7 +689,7 @@ class KeywordLabelSerializer(serializers.ModelSerializer):
@extend_schema_field(str) @extend_schema_field(str)
def get_label(self, obj): def get_label(self, obj):
return str(obj) return obj.name
class Meta: class Meta:
list_serializer_class = SpaceFilterSerializer list_serializer_class = SpaceFilterSerializer
@@ -665,7 +706,7 @@ class KeywordSerializer(UniqueFieldsMixin, ExtendedRecipeMixin):
@extend_schema_field(str) @extend_schema_field(str)
def get_label(self, obj): def get_label(self, obj):
return str(obj) return obj.name
def create(self, validated_data): def create(self, validated_data):
# since multi select tags dont have id's # since multi select tags dont have id's
@@ -740,8 +781,9 @@ class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer):
fields = ('id', 'category', 'supermarket', 'order') 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) category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
shopping_lists = ShoppingListSerializer(many=True, required=False)
def create(self, validated_data): def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip() validated_data['name'] = validated_data['name'].strip()
@@ -752,7 +794,7 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer, OpenDataMo
class Meta: class Meta:
model = Supermarket 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): class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
@@ -836,7 +878,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand') substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False) substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
parent = IntegerField(read_only=True) parent = IntegerField(read_only=True)
shopping_lists = ShoppingListSerializer(many=True, required=False)
properties = PropertySerializer(many=True, allow_null=True, required=False) properties = PropertySerializer(many=True, allow_null=True, required=False)
properties_food_unit = UnitSerializer(allow_null=True, required=False) properties_food_unit = UnitSerializer(allow_null=True, required=False)
properties_food_amount = CustomDecimalField(required=False) properties_food_amount = CustomDecimalField(required=False)
@@ -947,7 +989,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
fields = ( fields = (
'id', 'name', 'plural_name', 'description', 'shopping', 'recipe', 'url', 'properties', 'properties_food_amount', 'properties_food_unit', 'fdc_id', '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', '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') read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@@ -1327,7 +1369,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
@extend_schema_field(bool) @extend_schema_field(bool)
def in_shopping(self, obj): def in_shopping(self, obj):
return ShoppingListRecipe.objects.filter(mealplan=obj.id).exists() return obj.shoppinglistrecipe_set.count() > 0
def create(self, validated_data): def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user validated_data['created_by'] = self.context['request'].user
@@ -1393,13 +1435,23 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ShoppingListRecipe 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',) read_only_fields = ('id', 'created_by',)
class FoodShoppingSerializer(serializers.ModelSerializer):
supermarket_category = SupermarketCategorySerializer(read_only=True)
shopping_lists = ShoppingListSerializer(read_only=True, many=True)
class Meta:
model = Food
fields = ('id', 'name', 'plural_name', 'supermarket_category', 'shopping_lists')
class ShoppingListEntrySerializer(WritableNestedModelSerializer): class ShoppingListEntrySerializer(WritableNestedModelSerializer):
food = FoodSerializer(allow_null=True) food = FoodShoppingSerializer(allow_null=True)
unit = UnitSerializer(allow_null=True, required=False) unit = UnitSerializer(allow_null=True, required=False)
shopping_lists = ShoppingListSerializer(many=True, required=False)
list_recipe_data = ShoppingListRecipeSerializer(source='list_recipe', read_only=True) list_recipe_data = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField() amount = CustomDecimalField()
created_by = UserSerializer(read_only=True) created_by = UserSerializer(read_only=True)
@@ -1448,7 +1500,13 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
created_by=self.context['request'].user) created_by=self.context['request'].user)
del validated_data['mealplan_id'] 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): def update(self, instance, validated_data):
user = self.context['request'].user user = self.context['request'].user
@@ -1468,7 +1526,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
class Meta: class Meta:
model = ShoppingListEntry model = ShoppingListEntry
fields = ( 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' 'list_recipe_data', 'created_by', 'created_at', 'updated_at', 'completed_at', 'delay_until', 'mealplan_id'
) )
read_only_fields = ('id', 'created_by', 'created_at') read_only_fields = ('id', 'created_by', 'created_at')
@@ -1729,6 +1787,7 @@ class GenericModelReferenceSerializer(serializers.Serializer):
model = serializers.CharField() model = serializers.CharField()
name = serializers.CharField() name = serializers.CharField()
# Export/Import Serializers # Export/Import Serializers
class KeywordExportSerializer(KeywordSerializer): class KeywordExportSerializer(KeywordSerializer):

View File

@@ -40,6 +40,7 @@ router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'unit-conversion', api.UnitConversionViewSet) router.register(r'unit-conversion', api.UnitConversionViewSet)
router.register(r'property-type', api.PropertyTypeViewSet) # NOTE: if regenerating the legacy API these need renamed to food-property router.register(r'property-type', api.PropertyTypeViewSet) # NOTE: if regenerating the legacy API these need renamed to food-property
router.register(r'property', api.PropertyViewSet) router.register(r'property', api.PropertyViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet) router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet) router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
router.register(r'space', api.SpaceViewSet) router.register(r'space', api.SpaceViewSet)

View File

@@ -88,7 +88,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo
RecipeBookEntry, ShareLink, ShoppingListEntry, RecipeBookEntry, ShareLink, ShoppingListEntry,
ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider UserFile, UserPreference, UserSpace, ViewLog, RecipeImport, SearchPreference, SearchFields, AiLog, AiProvider, ShoppingList
) )
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
@@ -114,7 +114,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer, LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer,
AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer, AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer,
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer, RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer,
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer, GenericModelReferenceSerializer AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer, GenericModelReferenceSerializer, ShoppingListSerializer
) )
from cookbook.version_info import TANDOOR_VERSION from cookbook.version_info import TANDOOR_VERSION
from cookbook.views.import_export import get_integration from cookbook.views.import_export import get_integration
@@ -307,7 +307,8 @@ class FuzzyFilterMixin(viewsets.ModelViewSet, ExtendedRecipeMixin):
filter = Q(name__icontains=query) filter = Q(name__icontains=query)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
if any([self.model.__name__.lower() in x for x in if any([self.model.__name__.lower() in x for x in
self.request.user.searchpreference.unaccent.values_list('field', flat=True)]): self.request.user.searchpreference.unaccent.values_list('field', flat=True)]) and (
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'):
filter |= Q(name__unaccent__icontains=query) filter |= Q(name__unaccent__icontains=query)
self.queryset = ( self.queryset = (
@@ -1991,25 +1992,39 @@ class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
if serializer.is_valid(): if serializer.is_valid():
entries = [] entries = []
for e in serializer.validated_data['entries']: for e in serializer.validated_data['entries']:
entries.append( entry = ShoppingListEntry(
ShoppingListEntry( list_recipe_id=obj.pk,
list_recipe_id=obj.pk, amount=e['amount'],
amount=e['amount'], unit_id=e['unit_id'],
unit_id=e['unit_id'], food_id=e['food_id'],
food_id=e['food_id'], ingredient_id=e['ingredient_id'],
ingredient_id=e['ingredient_id'], created_by_id=request.user.id,
created_by_id=request.user.id, space_id=request.space.id,
space_id=request.space.id,
)
) )
entries.append(entry)
ShoppingListEntry.objects.bulk_create(entries) 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) ConnectorManager.add_work(ActionType.CREATED, *entries)
return Response(serializer.validated_data) return Response(serializer.validated_data)
else: else:
return Response(serializer.errors, 400) return Response(serializer.errors, 400)
class ShoppingListViewSet(LoggingMixin, viewsets.ModelViewSet, DeleteRelationMixing):
queryset = ShoppingList.objects
serializer_class = ShoppingListSerializer
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
pagination_class = DefaultPagination
def get_queryset(self):
queryset = self.queryset.filter(space=self.request.space).all()
return queryset
@extend_schema_view(list=extend_schema(parameters=[ @extend_schema_view(list=extend_schema(parameters=[
OpenApiParameter(name='updated_after', OpenApiParameter(name='updated_after',
description=_('Returns only elements updated after the given timestamp in ISO 8601 format.'), description=_('Returns only elements updated after the given timestamp in ISO 8601 format.'),
@@ -2029,19 +2044,23 @@ class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space) self.queryset = self.queryset.filter(space=self.request.space)
# select_related("list_recipe")
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
Q(created_by=self.request.user) Q(created_by=self.request.user)
| Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by', 'food', | Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by',
'food__properties', 'food',
'food__properties__property_type', 'food__shopping_lists',
'food__inherit_fields', 'shopping_lists',
'food__supermarket_category', 'unit',
'food__onhand_users', 'list_recipe',
'food__substitute', 'list_recipe__recipe__keywords',
'food__child_inherit_fields', 'list_recipe__recipe__created_by',
'unit', 'list_recipe',
'list_recipe__mealplan', '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',
'list_recipe__mealplan__recipe__keywords',
).distinct().all() ).distinct().all()
updated_after = self.request.query_params.get('updated_after', None) updated_after = self.request.query_params.get('updated_after', None)
@@ -3056,11 +3075,20 @@ def meal_plans_to_ical(queryset, filename):
for p in queryset: for p in queryset:
event = Event() event = Event()
event['uid'] = p.id event['uid'] = p.id
event.add('dtstart', p.from_date)
start_date_time = p.from_date
end_date_time = p.from_date
if p.to_date: if p.to_date:
event.add('dtend', p.to_date) end_date_time = p.to_date
else:
event.add('dtend', p.from_date) if p.meal_type.time:
start_date_time = datetime.datetime.combine(p.from_date, p.meal_type.time)
end_date_time = datetime.datetime.combine(p.to_date, p.meal_type.time) + datetime.timedelta(minutes=60)
event.add('dtstart', start_date_time)
event.add('dtend', end_date_time)
event['summary'] = f'{p.meal_type.name}: {p.get_label()}' event['summary'] = f'{p.meal_type.name}: {p.get_label()}'
event['description'] = p.note event['description'] = p.note
cal.add_component(event) cal.add_component(event)

View File

@@ -43,7 +43,10 @@ def index(request, path=None, resource=None):
return HttpResponseRedirect(reverse_lazy('view_setup')) return HttpResponseRedirect(reverse_lazy('view_setup'))
if 'signup_token' in request.session: if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')])) value = request.session['signup_token']
del request.session['signup_token']
request.session.modified = True
return HttpResponseRedirect(reverse('view_invite', args=[value]))
if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'): if request.user.is_authenticated or re.search(r'/recipe/\d+/', request.path[:512]) and request.GET.get('share'):
return render(request, 'frontend/tandoor.html', {}) return render(request, 'frontend/tandoor.html', {})

View File

@@ -675,4 +675,4 @@ DISABLE_EXTERNAL_CONNECTORS = extract_bool('DISABLE_EXTERNAL_CONNECTORS', False)
EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100)) EXTERNAL_CONNECTORS_QUEUE_SIZE = int(os.getenv('EXTERNAL_CONNECTORS_QUEUE_SIZE', 100))
mimetypes.add_type("text/javascript", ".js", True) mimetypes.add_type("text/javascript", ".js", True)
mimetypes.add_type("text/javascript", ".mjs", True) mimetypes.add_type("text/javascript", ".mjs", True)

View File

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

View File

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

View File

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

View File

@@ -10,15 +10,15 @@
<!-- <v-text-field density="compact" variant="outlined" class="pt-2 pb-2" :label="$t('Search')" hide-details clearable></v-text-field>--> <!-- <v-text-field density="compact" variant="outlined" class="pt-2 pb-2" :label="$t('Search')" hide-details clearable></v-text-field>-->
<!-- </v-list-item>--> <!-- </v-list-item>-->
<!-- <v-divider></v-divider>--> <!-- <v-divider></v-divider>-->
<v-list-item link title="Start" @click="window = 'start'" prepend-icon="fa-solid fa-house"></v-list-item> <v-list-item link :title="$t('Start')" @click="window = 'start'" prepend-icon="fa-solid fa-house"></v-list-item>
<v-list-item link title="Space" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item> <v-list-item link :title="$t('Space')" @click="window = 'space'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Recipes')" @click="window = 'recipes'" prepend-icon="$recipes"></v-list-item> <v-list-item link :title="$t('Recipes')" @click="window = 'recipes'" prepend-icon="$recipes"></v-list-item>
<v-list-item link :title="$t('Import')" @click="window = 'import'" prepend-icon="$import"></v-list-item> <v-list-item link :title="$t('Import')" @click="window = 'import'" prepend-icon="$import"></v-list-item>
<v-list-item link :title="$t('AI')" @click="window = 'ai'" prepend-icon="$ai"></v-list-item> <v-list-item link :title="$t('AI')" @click="window = 'ai'" prepend-icon="$ai"></v-list-item>
<v-list-item link :title="$t('Unit')" @click="window = 'unit'" prepend-icon="fa-solid fa-scale-balanced"></v-list-item> <v-list-item link :title="$t('Unit')" @click="window = 'unit'" prepend-icon="fa-solid fa-scale-balanced"></v-list-item>
<v-list-item link :title="$t('Food')" @click="window = 'food'" prepend-icon="fa-solid fa-carrot"></v-list-item> <v-list-item link :title="$t('Food')" @click="window = 'food'" prepend-icon="fa-solid fa-carrot"></v-list-item>
<v-list-item link :title="$t('Keyword')" @click="window = 'keyword'" prepend-icon="fa-solid fa-tags"></v-list-item> <v-list-item link :title="$t('Keyword')" @click="window = 'keyword'" prepend-icon="fa-solid fa-tags"></v-list-item>
<v-list-item link title="Recipe Structure" @click="window = 'recipe_structure'" prepend-icon="fa-solid fa-diagram-project"></v-list-item> <v-list-item link :title="$t('Recipe Structure')" @click="window = 'recipe_structure'" prepend-icon="fa-solid fa-diagram-project"></v-list-item>
<v-list-item link :title="$t('Properties')" @click="window = 'properties'" prepend-icon="fa-solid fa-database"></v-list-item> <v-list-item link :title="$t('Properties')" @click="window = 'properties'" prepend-icon="fa-solid fa-database"></v-list-item>
<v-list-item link :title="$t('Search')" @click="window = 'recipe_search'" prepend-icon="$search"></v-list-item> <v-list-item link :title="$t('Search')" @click="window = 'recipe_search'" prepend-icon="$search"></v-list-item>
<v-list-item link :title="$t('SavedSearch')" @click="window = 'search_filter'" prepend-icon="fa-solid fa-sd-card"></v-list-item> <v-list-item link :title="$t('SavedSearch')" @click="window = 'search_filter'" prepend-icon="fa-solid fa-sd-card"></v-list-item>
@@ -31,6 +31,8 @@
<v-main> <v-main>
<v-container> <v-container>
<v-select v-model="window" :items="mobileMenuItems" class="d-block d-lg-none"> </v-select>
<v-window v-model="window"> <v-window v-model="window">
<v-window-item value="start"> <v-window-item value="start">
<h2>Welcome to Tandoor 2</h2> <h2>Welcome to Tandoor 2</h2>
@@ -46,7 +48,8 @@
<v-btn class="mt-2 ms-2" color="info" href="https://github.com/TandoorRecipes/recipes" target="_blank" prepend-icon="fa-solid fa-code-branch">GitHub <v-btn class="mt-2 ms-2" color="info" href="https://github.com/TandoorRecipes/recipes" target="_blank" prepend-icon="fa-solid fa-code-branch">GitHub
</v-btn> </v-btn>
<v-alert class="mt-3" border="start" variant="tonal" color="success" v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)"> <v-alert class="mt-3" border="start" variant="tonal" color="success"
v-if="(!useUserPreferenceStore().serverSettings.hosted && !useUserPreferenceStore().activeSpace.demo)">
<v-alert-title>Did you know?</v-alert-title> <v-alert-title>Did you know?</v-alert-title>
Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend Tandoor is Open Source and available to anyone for free to host on their own server. Thousands of hours have been spend
making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort. making Tandoor what it is today. You can help make Tandoor even better by contributing or helping financing the effort.
@@ -60,10 +63,12 @@
</v-window-item> </v-window-item>
<v-window-item value="space"> <v-window-item value="space">
<p class="mt-3">All your data is stored in a Space where you can invite other people to collaborate on your recipe database. Typcially the members of a space <p class="mt-3">All your data is stored in a Space where you can invite other people to collaborate on your recipe database. Typcially the members of a
space
belong to one family/household/organization.</p> belong to one family/household/organization.</p>
<p class="mt-3">While everyone can access all recipes by default, Books, Shopping Lists and Mealplans are not shared by default. You can share them with other <p class="mt-3">While everyone can access all recipes by default, Books, Shopping Lists and Mealplans are not shared by default. You can share them with
other
members of your space members of your space
using the settings. using the settings.
</p> </p>
@@ -77,19 +82,24 @@
</v-window-item> </v-window-item>
<v-window-item value="recipes"> <v-window-item value="recipes">
<p class="mt-3">Recipes are the foundation of your Tandoor space. A Recipe has one or more steps that contain ingredients, instructions and other information. <p class="mt-3">Recipes are the foundation of your Tandoor space. A Recipe has one or more steps that contain ingredients, instructions and other
information.
Ingredients in turn consist of an amount, a unit and a food, allowing recipes to be scaled, nutrition's to be calculated and shopping to be organized. Ingredients in turn consist of an amount, a unit and a food, allowing recipes to be scaled, nutrition's to be calculated and shopping to be organized.
</p> </p>
<p class="mt-3">Besides manually creating them you can also import them from various different places. <p class="mt-3">Besides manually creating them you can also import them from various different places.
</p> </p>
<p class="mt-3">Recipes, by default, are visible to all members of your space. Setting them to private means only you can see it. After setting it to private you <p class="mt-3">Recipes, by default, are visible to all members of your space. Setting them to private means only you can see it. After setting it to
private you
can manually specify the people who should be able to view the recipe. can manually specify the people who should be able to view the recipe.
You can also create a share link for the recipe to share it with everyone that has access to the link. You can also create a share link for the recipe to share it with everyone that has access to the link.
</p> </p>
<p class="mt-3"></p> <p class="mt-3"></p>
<v-btn color="primary" variant="tonal" prepend-icon="$create" class="me-2" :to="{name: 'ModelEditPage', params: {model: 'Recipe'}}">{{ $t('Create') }}</v-btn> <v-btn color="primary" variant="tonal" prepend-icon="$create" class="me-2" :to="{name: 'ModelEditPage', params: {model: 'Recipe'}}">{{
$t('Create')
}}
</v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="$search" class="me-2" :to="{name: 'SearchPage'}">{{ $t('Search') }}</v-btn> <v-btn color="primary" variant="tonal" prepend-icon="$search" class="me-2" :to="{name: 'SearchPage'}">{{ $t('Search') }}</v-btn>
</v-window-item> </v-window-item>
@@ -119,7 +129,8 @@
</p> </p>
<p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted"> <p class="mt-3" v-if="useUserPreferenceStore().serverSettings.hosted">
To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space (either monthly or using a balance). To prevent accidental AI cost you can review your AI usage using the AI Log. The Server Administrator can also set AI usage limits for your space
(either monthly or using a balance).
</p> </p>
<p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted"> <p class="mt-3" v-if="!useUserPreferenceStore().serverSettings.hosted">
Depending on your subscription you will have different AI Credits available for your space every month. Additionally you might have a Credit balance Depending on your subscription you will have different AI Credits available for your space every month. Additionally you might have a Credit balance
@@ -153,7 +164,8 @@
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-scale-balanced" class="me-2" :to="{name: 'ModelListPage', params: {model: 'Unit'}}"> <v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-scale-balanced" class="me-2" :to="{name: 'ModelListPage', params: {model: 'Unit'}}">
{{ $t('Unit') }} {{ $t('Unit') }}
</v-btn> </v-btn>
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-exchange-alt" class="me-2" :to="{name: 'ModelListPage', params: {model: 'UnitConversion'}}"> <v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-exchange-alt" class="me-2"
:to="{name: 'ModelListPage', params: {model: 'UnitConversion'}}">
{{ $t('Conversion') }} {{ $t('Conversion') }}
</v-btn> </v-btn>
@@ -223,7 +235,8 @@
calculate the property amount if a Food is given in a different unit (e.g. 1kg or 1 cup). calculate the property amount if a Food is given in a different unit (e.g. 1kg or 1 cup).
</p> </p>
<v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-database" class="me-2 mt-2 mb-2" :to="{name: 'ModelListPage', params: {model: 'PropertyType'}}"> <v-btn color="primary" variant="tonal" prepend-icon="fa-solid fa-database" class="me-2 mt-2 mb-2"
:to="{name: 'ModelListPage', params: {model: 'PropertyType'}}">
{{ $t('Property') }} {{ $t('Property') }}
</v-btn> </v-btn>
<h3>Editor</h3> <h3>Editor</h3>
@@ -294,7 +307,8 @@
</p> </p>
<p class="mt-3"> <p class="mt-3">
You can assign Supermarket Categories to your Foods, either trough the Food Editor or directly by clicking on a Shopping List Entry, to automatically sort the list You can assign Supermarket Categories to your Foods, either trough the Food Editor or directly by clicking on a Shopping List Entry, to automatically
sort the list
according to the Category Order defined in the Supermarket. according to the Category Order defined in the Supermarket.
</p> </p>
@@ -333,7 +347,8 @@
<p class="mt-3"> <p class="mt-3">
When selecting a Recipe in a Meal Plan you can automatically add its ingredients to the shopping list. You can also manually add more entries trough the When selecting a Recipe in a Meal Plan you can automatically add its ingredients to the shopping list. You can also manually add more entries trough the
shopping tab in the Meal Plan editor. When deleting a Meal Plan all Shopping List Entries associated with that Meal Plan are deleted as well. When changing the shopping tab in the Meal Plan editor. When deleting a Meal Plan all Shopping List Entries associated with that Meal Plan are deleted as well. When
changing the
number of servings in a Meal Plan the Servings of the connected Recipe in the Shopping list are automatically changed as well. number of servings in a Meal Plan the Servings of the connected Recipe in the Shopping list are automatically changed as well.
</p> </p>
@@ -368,10 +383,30 @@
import {ref} from "vue"; import {ref} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
const drawer = defineModel() const drawer = defineModel()
const window = ref('start') const window = ref('start')
const mobileMenuItems = ref([
{title: t('Start'), props: {prependIcon: 'fa-solid fa-house'}, value: 'start'},
{title: t('Space'), props: {prependIcon: 'fa-solid fa-database'}, value: 'space'},
{title: t('Recipes'), props: {prependIcon: '$recipes'}, value: 'recipes'},
{title: t('Import'), props: {prependIcon: '$import'}, value: 'import'},
{title: t('AI'), props: {prependIcon: '$ai'}, value: 'ai'},
{title: t('Unit'), props: {prependIcon: 'fa-solid fa-scale-balanced'}, value: 'unit'},
{title: t('Food'), props: {prependIcon: 'fa-solid fa-carrot'}, value: 'food'},
{title: t('Keyword'), props: {prependIcon: 'fa-solid fa-tags'}, value: 'keyword'},
{title: t('RecipeStructure'), props: {prependIcon: 'fa-solid fa-diagram-project'}, value: 'recipe_structure'},
{title: t('Properties'), props: {prependIcon: 'fa-solid fa-database'}, value: 'properties'},
{title: t('Search'), props: {prependIcon: '$search'}, value: 'recipe_search'},
{title: t('SavedSearch'), props: {prependIcon: 'fa-solid fa-sd-card'}, value: 'search_filter'},
{title: t('Books'), props: {prependIcon: '$books'}, value: 'books'},
{title: t('Shopping'), props: {prependIcon: '$shopping'}, value: 'shopping'},
{title: t('Meal_Plan'), props: {prependIcon: '$mealplan'}, value: 'meal_plan'}
])
</script> </script>

View File

@@ -146,7 +146,11 @@ onMounted(() => {
function clickMealPlan(plan: MealPlan) { function clickMealPlan(plan: MealPlan) {
if (plan.recipe) { if (plan.recipe) {
router.push({name: 'RecipeViewPage', params: {id: plan.recipe.id}}) router.push({
name: 'RecipeViewPage',
params: { id: String(plan.recipe.id) }, // keep id in params
query: { servings: String(plan.servings ?? '') } // pass servings as query
})
} }
} }

View File

@@ -37,15 +37,15 @@
<v-dialog max-width="900px" v-model="dialog"> <v-dialog max-width="900px" v-model="dialog">
<v-card v-if="dialogProperty" :loading="loading"> <v-card v-if="dialogProperty" :loading="loading">
<v-closable-card-title :title="`${dialogProperty.propertyAmountTotal} ${dialogProperty.unit} ${dialogProperty.name}`" :sub-title="$t('total')" icon="$properties" <v-closable-card-title :title="`${dialogProperty.propertyAmountTotal} ${(dialogProperty.unit != null) ? dialogProperty.unit : ''} ${dialogProperty.name}`" :sub-title="$t('total')" icon="$properties"
v-model="dialog"></v-closable-card-title> v-model="dialog"></v-closable-card-title>
<v-card-text> <v-card-text>
<v-list> <v-list>
<v-list-item border v-for="fv in dialogProperty.foodValues" :key="`${dialogProperty.id}_${fv.id}`"> <v-list-item border v-for="fv in dialogProperty.foodValues" :key="`${dialogProperty.id}_${fv.id}`">
<template #prepend> <template #prepend>
<v-progress-circular size="55" width="5" :model-value="(fv.value/dialogProperty.propertyAmountTotal)*100" <v-progress-circular size="55" width="5" :model-value="(fv.value* props.ingredientFactor/dialogProperty.propertyAmountTotal)*100"
:color="colorScale((fv.value/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null && dialogProperty.propertyAmountTotal > 0"> :color="colorScale((fv.value* props.ingredientFactor/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null && dialogProperty.propertyAmountTotal > 0">
{{ Math.round((fv.value / dialogProperty.propertyAmountTotal) * 100) }}% {{ Math.round((fv.value* props.ingredientFactor / dialogProperty.propertyAmountTotal) * 100) }}%
</v-progress-circular> </v-progress-circular>
<v-progress-circular size="55" width="5" v-if="fv.value == null">?</v-progress-circular> <v-progress-circular size="55" width="5" v-if="fv.value == null">?</v-progress-circular>
</template> </template>
@@ -59,7 +59,7 @@
<model-edit-dialog model="UnitConversion" @create="refreshRecipe()" <model-edit-dialog model="UnitConversion" @create="refreshRecipe()"
:item-defaults="{baseAmount: 1, baseUnit: fv.missing_conversion.base_unit, convertedUnit: fv.missing_conversion.converted_unit, food: fv.food}"></model-edit-dialog> :item-defaults="{baseAmount: 1, baseUnit: fv.missing_conversion.base_unit, convertedUnit: fv.missing_conversion.converted_unit, food: fv.food}"></model-edit-dialog>
</v-chip> </v-chip>
<v-chip v-else-if="fv.value != undefined">{{ $n(fv.value) }} {{ dialogProperty.unit }}</v-chip> <v-chip v-else-if="fv.value != undefined">{{ $n(fv.value * props.ingredientFactor) }} {{ dialogProperty.unit }}</v-chip>
<v-chip color="warning" prepend-icon="$edit" class="cursor-pointer" :to="{name: 'ModelEditPage', params: {model: 'Recipe', id: recipe.id}}" v-else-if="fv.missing_unit"> <v-chip color="warning" prepend-icon="$edit" class="cursor-pointer" :to="{name: 'ModelEditPage', params: {model: 'Recipe', id: recipe.id}}" v-else-if="fv.missing_unit">
{{ $t('NoUnit') }} {{ $t('NoUnit') }}
</v-chip> </v-chip>
@@ -101,7 +101,10 @@ type PropertyWrapper = {
} }
const props = defineProps({ const props = defineProps({
servings: {type: Number, required: true,}, ingredientFactor: {
type: Number,
required: true,
},
}) })
const recipe = defineModel<Recipe>({required: true}) const recipe = defineModel<Recipe>({required: true})
@@ -143,7 +146,7 @@ const propertyList = computed(() => {
description: rp.propertyType.description, description: rp.propertyType.description,
foodValues: [], foodValues: [],
propertyAmountPerServing: rp.propertyAmount, propertyAmountPerServing: rp.propertyAmount,
propertyAmountTotal: rp.propertyAmount * recipe.value.servings * (props.servings / recipe.value.servings), propertyAmountTotal: rp.propertyAmount * recipe.value.servings * props.ingredientFactor,
missingValue: false, missingValue: false,
unit: rp.propertyType.unit, unit: rp.propertyType.unit,
type: rp.propertyType, type: rp.propertyType,
@@ -161,7 +164,7 @@ const propertyList = computed(() => {
icon: fp.icon, icon: fp.icon,
foodValues: fp.food_values, foodValues: fp.food_values,
propertyAmountPerServing: fp.total_value / recipe.value.servings, propertyAmountPerServing: fp.total_value / recipe.value.servings,
propertyAmountTotal: fp.total_value * (props.servings / recipe.value.servings), propertyAmountTotal: fp.total_value * props.ingredientFactor,
missingValue: fp.missing_value, missingValue: fp.missing_value,
unit: fp.unit, unit: fp.unit,
type: fp, type: fp,

View File

@@ -69,7 +69,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {onMounted, PropType, ref} from "vue"; import {onMounted, PropType, ref, watch} from "vue";
import {ApiApi, CookLog, Recipe} from "@/openapi"; import {ApiApi, CookLog, Recipe} from "@/openapi";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore"; import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
@@ -82,6 +82,10 @@ const props = defineProps({
type: Object as PropType<Recipe>, type: Object as PropType<Recipe>,
required: true required: true
}, },
servings: {
type: Number,
required: true
}
}) })
const newCookLog = ref({} as CookLog); const newCookLog = ref({} as CookLog);
@@ -121,7 +125,7 @@ function recLoadCookLog(recipeId: number, page: number = 1) {
*/ */
function resetForm() { function resetForm() {
newCookLog.value = {} as CookLog newCookLog.value = {} as CookLog
newCookLog.value.servings = props.recipe.servings newCookLog.value.servings = props.servings
newCookLog.value.createdAt = new Date() newCookLog.value.createdAt = new Date()
newCookLog.value.recipe = props.recipe.id! newCookLog.value.recipe = props.recipe.id!
} }
@@ -140,6 +144,13 @@ function saveCookLog() {
}) })
} }
/**
* watch for changes in servings prop and update the servings input field
*/
watch(() => props.servings, (newVal) => {
newCookLog.value.servings = newVal
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,7 +1,7 @@
<template> <template>
<template v-if="!props.loading"> <template v-if="!props.loading">
<router-link :to="{name: 'RecipeViewPage', params: {id: props.recipe.id}}" :target="linkTarget"> <router-link :to="dest" :target="linkTarget">
<recipe-image :style="{height: props.height}" :recipe="props.recipe" rounded="lg" class="mr-3 ml-3"> <recipe-image :style="{height: props.height}" :recipe="props.recipe" rounded="lg" class="mr-3 ml-3">
</recipe-image> </recipe-image>
@@ -36,7 +36,7 @@
</div> </div>
<v-card :to="{name: 'RecipeViewPage', params: {id: props.recipe.id}}" :style="{'height': props.height}" v-if="false"> <v-card :to="dest" :style="{'height': props.height}" v-if="false">
<v-tooltip <v-tooltip
class="align-center justify-center" class="align-center justify-center"
location="top center" origin="overlap" location="top center" origin="overlap"
@@ -97,7 +97,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {PropType} from 'vue' import {computed, PropType} from 'vue'
import KeywordsComponent from "@/components/display/KeywordsBar.vue"; import KeywordsComponent from "@/components/display/KeywordsBar.vue";
import {Recipe, RecipeOverview} from "@/openapi"; import {Recipe, RecipeOverview} from "@/openapi";
@@ -113,20 +113,29 @@ const props = defineProps({
show_description: {type: Boolean, required: false}, show_description: {type: Boolean, required: false},
height: {type: String, required: false, default: '15vh'}, height: {type: String, required: false, default: '15vh'},
linkTarget: {type: String, required: false, default: ''}, linkTarget: {type: String, required: false, default: ''},
showMenu: {type: Boolean, default: true, required: false} showMenu: {type: Boolean, default: true, required: false},
servings: {type: Number, required: false},
}) })
const router = useRouter() const router = useRouter()
const dest = computed(() => {
const route: any = { name: 'RecipeViewPage', params: { id: props.recipe.id } };
if (props.servings !== undefined) {
route.query = { servings: String(props.servings) };
}
return route;
})
/** /**
* open the recipe either in the same tab or in a new tab depending on the link target prop * open the recipe either in the same tab or in a new tab depending on the link target prop
*/ */
function openRecipe() { function openRecipe() {
if (props.linkTarget != '') { if (props.linkTarget != '') {
const routeData = router.resolve({name: 'RecipeViewPage', params: {id: props.recipe.id}}); const routeData = router.resolve(dest.value);
window.open(routeData.href, props.linkTarget); window.open(routeData.href, props.linkTarget);
} else { } else {
router.push({name: 'RecipeViewPage', params: {id: props.recipe.id}}) router.push(dest.value);
} }
} }

View File

@@ -10,7 +10,7 @@
<template v-if="recipe.name != undefined"> <template v-if="recipe.name != undefined">
<template class="d-block d-lg-none"> <template class="d-block d-lg-none d-print-none">
<!-- mobile layout --> <!-- mobile layout -->
<v-card class="rounded-0"> <v-card class="rounded-0">
@@ -25,7 +25,7 @@
<span class="ps-2 text-h5 flex-grow-1 pa-1" :class="{'text-truncate': !showFullRecipeName}" @click="showFullRecipeName = !showFullRecipeName"> <span class="ps-2 text-h5 flex-grow-1 pa-1" :class="{'text-truncate': !showFullRecipeName}" @click="showFullRecipeName = !showFullRecipeName">
{{ recipe.name }} {{ recipe.name }}
</span> </span>
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated"></recipe-context-menu> <recipe-context-menu :recipe="recipe" :servings="servings" v-if="useUserPreferenceStore().isAuthenticated"></recipe-context-menu>
</v-sheet> </v-sheet>
<keywords-component variant="flat" class="ms-1" :keywords="recipe.keywords"></keywords-component> <keywords-component variant="flat" class="ms-1" :keywords="recipe.keywords"></keywords-component>
<private-recipe-badge :users="recipe.shared" v-if="recipe._private"></private-recipe-badge> <private-recipe-badge :users="recipe.shared" v-if="recipe._private"></private-recipe-badge>
@@ -61,7 +61,7 @@
</v-card> </v-card>
</template> </template>
<!-- Desktop horizontal layout --> <!-- Desktop horizontal layout -->
<template class="d-none d-lg-block"> <template class="d-none d-lg-block d-print-block">
<v-row dense> <v-row dense>
<v-col cols="8"> <v-col cols="8">
<recipe-image <recipe-image
@@ -75,7 +75,7 @@
<v-card-text class="flex-grow-1"> <v-card-text class="flex-grow-1">
<div class="d-flex"> <div class="d-flex">
<h1 class="flex-column flex-grow-1">{{ recipe.name }}</h1> <h1 class="flex-column flex-grow-1">{{ recipe.name }}</h1>
<recipe-context-menu :recipe="recipe" v-if="useUserPreferenceStore().isAuthenticated" <recipe-context-menu :recipe="recipe" :servings="servings" v-if="useUserPreferenceStore().isAuthenticated"
class="flex-column mb-auto mt-2 float-right"></recipe-context-menu> class="flex-column mb-auto mt-2 float-right"></recipe-context-menu>
</div> </div>
<p> <p>
@@ -118,7 +118,7 @@
</v-row> </v-row>
</template> </template>
<template v-if="recipe.filePath"> <template v-if="recipe.filePath && !useUserPreferenceStore().isPrintMode">
<external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer> <external-recipe-viewer class="mt-2" :recipe="recipe"></external-recipe-viewer>
<v-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled" <v-card :title="$t('AI')" prepend-icon="$ai" :loading="fileApiLoading || loading" :disabled="fileApiLoading || loading || !useUserPreferenceStore().activeSpace.aiEnabled"
@@ -144,7 +144,7 @@
<step-view v-model="recipe.steps[index]" :step-number="index+1" :ingredientFactor="ingredientFactor"></step-view> <step-view v-model="recipe.steps[index]" :step-number="index+1" :ingredientFactor="ingredientFactor"></step-view>
</v-card> </v-card>
<property-view v-model="recipe" :servings="servings"></property-view> <property-view v-model="recipe" :ingredientFactor="ingredientFactor"></property-view>
<v-card class="mt-2"> <v-card class="mt-2">
<v-card-text> <v-card-text>
@@ -190,7 +190,7 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<recipe-activity :recipe="recipe" v-if="useUserPreferenceStore().userSettings.comments"></recipe-activity> <recipe-activity :recipe="recipe" :servings="servings" v-if="useUserPreferenceStore().userSettings.comments"></recipe-activity>
</template> </template>
</template> </template>
@@ -220,8 +220,11 @@ const {doAiImport, fileApiLoading} = useFileApi()
const loading = ref(false) const loading = ref(false)
const recipe = defineModel<Recipe>({required: true}) const recipe = defineModel<Recipe>({required: true})
const props = defineProps<{
servings: {type: Number, required: false},
}>()
const servings = ref(1) const servings = ref(props.servings ?? recipe.value.servings ?? 1)
const showFullRecipeName = ref(false) const showFullRecipeName = ref(false)
const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider) const selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
@@ -236,11 +239,13 @@ const ingredientFactor = computed(() => {
/** /**
* change servings when recipe servings are changed * change servings when recipe servings are changed
*/ */
watch(() => recipe.value.servings, () => { if (props.servings === undefined) {
if (recipe.value.servings) { watch(() => recipe.value.servings, () => {
servings.value = recipe.value.servings if (recipe.value.servings) {
} servings.value = recipe.value.servings
}) }
})
}
onMounted(() => { onMounted(() => {
//keep screen on while viewing a recipe //keep screen on while viewing a recipe

View File

@@ -1,11 +1,17 @@
<template> <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-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0 shopping-border"
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)" :id="itemContainerId"
@touchend="handleSwipe()"
@click="dialog = true;"
:value="shoppingListFood"
> >
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">--> <!-- <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>--> <!-- <i class="swipe-icon fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
<!-- </div>--> <!-- </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="flex-grow-1 p-2">
<div class="d-flex"> <div class="d-flex">
@@ -31,13 +37,18 @@
</div> </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]> <template v-slot:[checkBtnSlot]>
<div class="ps-3 pe-3" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);"> <div class="ps-3 pe-3" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);">
<v-btn color="success" size="large" <v-btn color="success" size="large"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain"> :class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn> </v-btn>
</div> </div>
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
</template> </template>
<!-- <div class="swipe-action bg-primary justify-content-end">--> <!-- <div class="swipe-action bg-primary justify-content-end">-->
@@ -56,20 +67,23 @@ import {computed, PropType, ref} from "vue";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import {useShoppingStore} from "@/stores/ShoppingStore.js"; import {useShoppingStore} from "@/stores/ShoppingStore.js";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.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 {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping"; import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping";
import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils"; import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils";
import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue"; import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue";
import {pluralString} from "@/utils/model_utils.ts"; import {pluralString} from "@/utils/model_utils.ts";
import ShoppingListsBar from "@/components/display/ShoppingListsBar.vue";
const emit = defineEmits(['clicked']) const emit = defineEmits(['clicked'])
const props = defineProps({ const props = defineProps({
shoppingListFood: {type: {} as PropType<IShoppingListFood>, required: true}, 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 checkBtnSlot = ref(useUserPreferenceStore().userSettings.leftHanded ? 'prepend' : 'append')
const selectBtnSlot = ref(useUserPreferenceStore().userSettings.leftHanded ? 'append' : 'prepend')
const dialog = ref(false) const dialog = ref(false)
@@ -82,9 +96,7 @@ const entries = computed(() => {
*/ */
const itemContainerId = computed(() => { const itemContainerId = computed(() => {
let id = 'id_sli_' let id = 'id_sli_'
for (let i in entries.value) { entries.value.forEach(e => id += e.id + '_')
id += i + '_'
}
return 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 * calculate the amounts for the given line
* can combine 1 to n entries with the same unit * can combine 1 to n entries with the same unit
@@ -123,34 +151,34 @@ const amounts = computed((): ShoppingLineAmount[] => {
for (let i in entries.value) { for (let i in entries.value) {
let e = entries.value[i] 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 if (e.amount > 0) {
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) { let uaMerged = false
unitAmounts.push({ unitAmounts.forEach(ua => {
key: `${unit}_${e.checked}_${isDelayed(e)}`, if (((ua.unit == null && e.unit == null) || (ua.unit != null && ua.unit.id! == unit)) && ua.checked == e.checked && ua.delayed == isDelayed(e)) {
amount: e.amount, ua.amount += e.amount
unit: e.unit, uaMerged = true
checked: e.checked,
delayed: isDelayed(e)
} as ShoppingLineAmount)
} }
})
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 return unitAmounts
}) })
@@ -171,29 +199,28 @@ const infoRow = computed(() => {
for (let i in entries.value) { for (let i in entries.value) {
let e = entries.value[i] 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) { if (useUserPreferenceStore().deviceSettings.shopping_item_info_created_by && authors.length > 0) {
@@ -247,4 +274,22 @@ function handleSwipe() {
<style> <style>
/* TODO swipe system classes removed because not working (visually, touch detection was working), retrieve from old ShoppingLineItem VCS */ /* 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> </style>

View File

@@ -2,7 +2,7 @@
<v-tabs v-model="currentTab"> <v-tabs v-model="currentTab">
<v-tab value="shopping"><i class="fas fa-fw" <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="{'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">{{ <v-tab value="recipes"><i class="fas fa-book fa-fw"></i> <span class="d-none d-md-block ms-1">{{
$t('Recipes') $t('Recipes')
}} ({{ useShoppingStore().getAssociatedRecipes().length }})</span></v-tab> }} ({{ useShoppingStore().getAssociatedRecipes().length }})</span></v-tab>
@@ -25,9 +25,13 @@
<v-list density="compact"> <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="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-divider></v-divider>
<v-list-item> <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-select>
</v-list-item> </v-list-item>
<v-list-item v-if="useUserPreferenceStore().deviceSettings.shopping_selected_grouping == ShoppingGroupingOptions.CATEGORY"> <v-list-item v-if="useUserPreferenceStore().deviceSettings.shopping_selected_grouping == ShoppingGroupingOptions.CATEGORY">
@@ -65,29 +69,94 @@
</v-list> </v-list>
</v-menu> </v-menu>
<v-btn height="100%" rounded="0" variant="plain"> <!-- <v-btn height="100%" rounded="0" variant="plain">-->
<i class="fa-solid fa-download"></i> <!-- <i class="fa-solid fa-download"></i>-->
<shopping-export-dialog></shopping-export-dialog> <!-- <shopping-export-dialog></shopping-export-dialog>-->
</v-btn> <!-- </v-btn>-->
<v-btn height="100%" rounded="0" variant="plain" @click="useShoppingStore().undoChange()"> <!-- <v-btn height="100%" rounded="0" variant="plain" @click="useShoppingStore().undoChange()">-->
<i class="fa-solid fa-arrow-rotate-left"></i> <!-- <i class="fa-solid fa-arrow-rotate-left"></i>-->
</v-btn> <!-- </v-btn>-->
</v-tabs> </v-tabs>
<shopping-export-dialog v-model="exportDialog" activator="model"></shopping-export-dialog>
<v-window v-model="currentTab"> <v-window v-model="currentTab">
<v-window-item value="shopping"> <v-window-item value="shopping">
<v-container> <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-row class="pa-0" dense>
<v-col class="pa-0"> <v-col class="pa-0">
<v-chip-group v-model="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket" v-if="supermarkets.length > 0"> <v-chip label density="compact" variant="outlined" style="max-width: 50%;" :prepend-icon="TSupermarket.icon" append-icon="fa-solid fa-caret-down">
<v-chip v-for="s in supermarkets" :value="s" :key="s.id" label density="compact" variant="outlined" color="primary">{{ s.name }}</v-chip> <span v-if="useUserPreferenceStore().deviceSettings.shopping_selected_supermarket != null">
</v-chip-group> {{ 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-col>
</v-row> </v-row>
<v-row> <v-row class="mt-0">
<v-col> <v-col>
<v-alert v-if="useShoppingStore().hasFailedItems()" color="warning" class="mb-2"> <v-alert v-if="useShoppingStore().hasFailedItems()" color="warning" class="mb-2">
<template #prepend> <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-skeleton-loader type="list-item"></v-skeleton-loader> <v-skeleton-loader type="list-item"></v-skeleton-loader>
</v-list> </v-list>
<v-list class="mt-3" density="compact" v-else> <v-list class="mt-3" density="compact" v-model:selected="selectedLines" select-strategy="leaf" v-else>
<template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name"> <template v-for="category in useShoppingStore().entriesByGroup" :key="category.name">
<template v-if="isShoppingCategoryVisible(category)">
<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"> <v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader>
<shopping-line-item :shopping-list-food="value"></shopping-line-item> <v-list-subheader v-else>{{ category.name }}</v-list-subheader>
</template> <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>
</template> </template>
</v-list> </v-list>
@@ -246,14 +314,14 @@
<script setup lang="ts"> <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 {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 {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue"; import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import ModelSelect from "@/components/inputs/ModelSelect.vue"; import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {ShoppingGroupingOptions} from "@/types/Shopping"; import {IShoppingListFood, ShoppingGroupingOptions} from "@/types/Shopping";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue"; import NumberScalerDialog from "@/components/inputs/NumberScalerDialog.vue";
import SupermarketEditor from "@/components/model_editors/SupermarketEditor.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 {isShoppingCategoryVisible} from "@/utils/logic_utils.ts";
import ShoppingExportDialog from "@/components/dialogs/ShoppingExportDialog.vue"; import ShoppingExportDialog from "@/components/dialogs/ShoppingExportDialog.vue";
import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue"; import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
import {TShoppingList, TSupermarket} from "@/types/Models.ts";
const {t} = useI18n() const {t} = useI18n()
const exportDialog = ref(false)
const currentTab = ref("shopping") const currentTab = ref("shopping")
const supermarkets = ref([] as Supermarket[]) const supermarkets = ref([] as Supermarket[])
const shoppingLists = ref([] as ShoppingList[])
const manualAddRecipe = ref<undefined | Recipe>(undefined) const manualAddRecipe = ref<undefined | Recipe>(undefined)
const selectedLines = shallowRef([] as IShoppingListFood[])
/** /**
* VSelect items for shopping list grouping options with localized names * VSelect items for shopping list grouping options with localized names
*/ */
@@ -283,6 +356,10 @@ const groupingOptionsItems = computed(() => {
return items return items
}) })
watch(() => useUserPreferenceStore().deviceSettings, () => {
useShoppingStore().updateEntriesStructure()
}, {deep: true})
onMounted(() => { onMounted(() => {
addEventListener("visibilitychange", (event) => { addEventListener("visibilitychange", (event) => {
useShoppingStore().autoSyncHasFocus = (document.visibilityState === 'visible') useShoppingStore().autoSyncHasFocus = (document.visibilityState === 'visible')
@@ -304,6 +381,7 @@ onMounted(() => {
} }
loadSupermarkets() 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> </script>
<style scoped> <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

@@ -22,10 +22,10 @@
<timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer> <timer :seconds="step.time != undefined ? step.time*60 : 0" @stop="timerRunning = false" v-if="timerRunning"></timer>
<v-card-text v-if="step.ingredients.length > 0 || step.instruction != ''"> <v-card-text v-if="step.ingredients.length > 0 || step.instruction != ''">
<v-row> <v-row>
<v-col cols="12" md="6" v-if="step.ingredients.length > 0 && (step.showIngredientsTable || step.show_ingredients_table)"> <v-col :cols="(useUserPreferenceStore().isPrintMode) ? 6 : 12" md="6" v-if="step.ingredients.length > 0 && (step.showIngredientsTable || step.show_ingredients_table)">
<ingredients-table v-model="step.ingredients" :ingredient-factor="ingredientFactor"></ingredients-table> <ingredients-table v-model="step.ingredients" :ingredient-factor="ingredientFactor"></ingredients-table>
</v-col> </v-col>
<v-col cols="12" md="6" class="markdown-body"> <v-col :cols="(useUserPreferenceStore().isPrintMode) ? 6 : 12" md="6" class="markdown-body">
<instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor" <instructions :instructions_html="step.instructionsMarkdown" :ingredient_factor="ingredientFactor"
v-if="step.instructionsMarkdown != undefined"></instructions> v-if="step.instructionsMarkdown != undefined"></instructions>
<!-- sub recipes dont have a correct schema, thus they use different variable naming --> <!-- sub recipes dont have a correct schema, thus they use different variable naming -->
@@ -62,6 +62,7 @@ import {Step} from "@/openapi";
import Instructions from "@/components/display/Instructions.vue"; import Instructions from "@/components/display/Instructions.vue";
import Timer from "@/components/display/Timer.vue"; import Timer from "@/components/display/Timer.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const step = defineModel<Step>({required: true}) const step = defineModel<Step>({required: true})

View File

@@ -26,7 +26,7 @@
<v-progress-circular v-if="duplicateLoading" indeterminate size="small"></v-progress-circular> <v-progress-circular v-if="duplicateLoading" indeterminate size="small"></v-progress-circular>
</template> </template>
</v-list-item> </v-list-item>
<v-list-item :to="{ name: 'RecipeViewPage', params: { id: recipe.id}, query: {print: 'true'} }" :active="false" target="_blank" prepend-icon="fa-solid fa-print"> <v-list-item :to="{ name: 'RecipeViewPage', params: { id: recipe.id}, query: {print: 'true', servings: props.servings} }" :active="false" target="_blank" prepend-icon="fa-solid fa-print">
{{ $t('Print') }} {{ $t('Print') }}
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -47,22 +47,21 @@ import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts"; import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useFileApi} from "@/composables/useFileApi.ts"; import {useFileApi} from "@/composables/useFileApi.ts";
import {useI18n} from "vue-i18n";
const router = useRouter() const router = useRouter()
const {t} = useI18n()
const {updateRecipeImage} = useFileApi() const {updateRecipeImage} = useFileApi()
const props = defineProps({ const props = defineProps({
recipe: {type: Object as PropType<Recipe | RecipeOverview>, required: true}, recipe: {type: Object as PropType<Recipe | RecipeOverview>, required: true},
servings: {type: Number, default: undefined},
size: {type: String, default: 'medium'}, size: {type: String, default: 'medium'},
}) })
const mealPlanDialog = ref(false) const mealPlanDialog = ref(false)
const duplicateLoading = ref(false) const duplicateLoading = ref(false)
function openPrintView() {
print()
}
/** /**
* create a duplicate of the recipe by pulling its current data and creating a new recipe with the same data * create a duplicate of the recipe by pulling its current data and creating a new recipe with the same data
*/ */
@@ -70,7 +69,27 @@ function duplicateRecipe() {
let api = new ApiApi() let api = new ApiApi()
duplicateLoading.value = true duplicateLoading.value = true
api.apiRecipeRetrieve({id: props.recipe.id!}).then(originalRecipe => { api.apiRecipeRetrieve({id: props.recipe.id!}).then(originalRecipe => {
api.apiRecipeCreate({recipe: originalRecipe}).then(newRecipe => {
let recipe = {...originalRecipe, ...{id: undefined, name: originalRecipe.name + `(${t('Copy')})`}}
recipe.steps = recipe.steps.map((step) => {
return {
...step,
...{
id: undefined,
ingredients: step.ingredients.map((ingredient) => {
return {...ingredient, ...{id: undefined}}
}),
},
}
})
if (recipe.properties != null) {
recipe.properties = recipe.properties.map((p) => {
return {...p, ...{id: undefined}}
})
}
api.apiRecipeCreate({recipe: recipe}).then(newRecipe => {
if (originalRecipe.image) { if (originalRecipe.image) {
updateRecipeImage(newRecipe.id!, null, originalRecipe.image).then(r => { updateRecipeImage(newRecipe.id!, null, originalRecipe.image).then(r => {

View File

@@ -283,8 +283,9 @@ function parseAndInsertIngredients() {
} }
}) })
Promise.allSettled(promises).then(r => { Promise.allSettled(promises).then(r => {
step.value.ingredients = step.value.ingredients.filter(i => i.food != null || i.note != null || i.amount != 0)
r.forEach(i => { r.forEach(i => {
console.log(i)
step.value.ingredients.push({ step.value.ingredients.push({
originalText: i.value.originalText, originalText: i.value.originalText,
amount: i.value.amount, amount: i.value.amount,

View File

@@ -30,6 +30,7 @@
<v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea> <v-textarea :label="$t('Description')" v-model="editingObj.description"></v-textarea>
<!-- TODO fix card overflow invisible, overflow-visible class is not working --> <!-- 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('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-form>
</v-tabs-window-item> </v-tabs-window-item>

View File

@@ -29,7 +29,7 @@
@update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect> @update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect>
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>--> <!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>-->
<!--TODO create days input with +/- synced to date --> <!--TODO create days input with +/- synced to date -->
<recipe-card :recipe="editingObj.recipe" v-if="editingObj && editingObj.recipe" link-target="_blank"></recipe-card> <recipe-card :recipe="editingObj.recipe" :servings="editingObj.servings" v-if="editingObj && editingObj.recipe" link-target="_blank"></recipe-card>
<v-btn prepend-icon="$shopping" color="create" class="mt-1" v-if="!editingObj.shopping && editingObj.recipe && isUpdate()"> <v-btn prepend-icon="$shopping" color="create" class="mt-1" v-if="!editingObj.shopping && editingObj.recipe && isUpdate()">
{{$t('Add')}} {{$t('Add')}}
<add-to-shopping-dialog :recipe="editingObj.recipe" :meal-plan="editingObj" @created="loadShoppingListEntries(); editingObj.shopping = true;"></add-to-shopping-dialog> <add-to-shopping-dialog :recipe="editingObj.recipe" :meal-plan="editingObj" @created="loadShoppingListEntries(); editingObj.shopping = true;"></add-to-shopping-dialog>

View File

@@ -0,0 +1,68 @@
<template>
<model-editor-base
:loading="loading"
:dialog="dialog"
@save="saveObject"
@delete="deleteObject"
@close="emit('close'); editingObjChanged = false"
:is-update="isUpdate()"
:is-changed="editingObjChanged"
:model-class="modelClass"
:object-name="editingObjName()"
:editing-object="editingObj">
<v-card-text>
<v-form :disabled="loading">
<v-text-field :label="$t('Name')" v-model="editingObj.name"></v-text-field>
<v-textarea :label="$t('Description')" v-model="editingObj.description" :rows="2" auto-grow></v-textarea>
<v-color-picker :label="$t('Color')" v-model="editingObj.color" mode="hex" :modes="['hex']" show-swatches
:swatches="[['#ddbf86'],['#b98766'],['#b55e4f'],['#82aa8b'],['#385f84']]"></v-color-picker>
</v-form>
</v-card-text>
</model-editor-base>
</template>
<script setup lang="ts">
import {onMounted, PropType, watch} from "vue";
import {ShoppingList, ShoppingListEntry} from "@/openapi";
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
import ModelSelect from "@/components/inputs/ModelSelect.vue";
const props = defineProps({
item: {type: {} as PropType<ShoppingList>, required: false, default: null},
itemId: {type: [Number, String], required: false, default: undefined},
itemDefaults: {type: {} as PropType<ShoppingList>, required: false, default: {} as ShoppingList},
dialog: {type: Boolean, default: false}
})
const emit = defineEmits(['create', 'save', 'delete', 'close', 'changedState'])
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, loading, editingObj, editingObjChanged, modelClass} = useModelEditorFunctions<ShoppingList>('ShoppingList', emit)
/**
* watch prop changes and re-initialize editor
* required to embed editor directly into pages and be able to change item from the outside
*/
watch([() => props.item, () => props.itemId], () => {
initializeEditor()
})
// object specific data (for selects/display)
onMounted(() => {
initializeEditor()
})
/**
* component specific state setup logic
*/
function initializeEditor(){
setupState(props.item, props.itemId, {itemDefaults: props.itemDefaults})
}
</script>
<style scoped>
</style>

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('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('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('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 <v-number-input
class="mt-2" class="mt-2"

View File

@@ -16,7 +16,7 @@ export function useNavigation() {
{component: VListItem, prependIcon: '$recipes', title: 'Home', to: {name: 'StartPage', params: {}}}, {component: VListItem, prependIcon: '$recipes', title: 'Home', to: {name: 'StartPage', params: {}}},
{component: VListItem, prependIcon: '$search', title: t('Search'), to: {name: 'SearchPage', params: {}}}, {component: VListItem, prependIcon: '$search', title: t('Search'), to: {name: 'SearchPage', params: {}}},
{component: VListItem, prependIcon: '$mealplan', title: t('Meal_Plan'), to: {name: 'MealPlanPage', params: {}}}, {component: VListItem, prependIcon: '$mealplan', title: t('Meal_Plan'), to: {name: 'MealPlanPage', params: {}}},
{component: VListItem, prependIcon: '$shopping', title: t('Shopping_list'), to: {name: 'ShoppingListPage', params: {}}}, {component: VListItem, prependIcon: '$shopping', title: t('Shopping'), to: {name: 'ShoppingListPage', params: {}}},
{component: VListItem, prependIcon: 'fas fa-globe', title: t('Import'), to: {name: 'RecipeImportPage', params: {}}}, {component: VListItem, prependIcon: 'fas fa-globe', title: t('Import'), to: {name: 'RecipeImportPage', params: {}}},
{component: VListItem, prependIcon: '$books', title: t('Books'), to: {name: 'BooksPage', params: {}}}, {component: VListItem, prependIcon: '$books', title: t('Books'), to: {name: 'BooksPage', params: {}}},
{component: VListItem, prependIcon: 'fa-solid fa-folder-tree', title: t('Database'), to: {name: 'DatabasePage', params: {}}}, {component: VListItem, prependIcon: 'fa-solid fa-folder-tree', title: t('Database'), to: {name: 'DatabasePage', params: {}}},

View File

@@ -23,6 +23,7 @@
"AiModelHelp": "", "AiModelHelp": "",
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"All": "",
"App": "", "App": "",
"Apply": "", "Apply": "",
"Are_You_Sure": "", "Are_You_Sure": "",
@@ -267,6 +268,7 @@
"Ratings": "", "Ratings": "",
"Recently_Viewed": "", "Recently_Viewed": "",
"Recipe": "", "Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "", "Recipe_Book": "",
"Recipe_Image": "", "Recipe_Image": "",
"Recipes": "", "Recipes": "",
@@ -314,6 +316,7 @@
"SpaceMembersHelp": "", "SpaceMembersHelp": "",
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Start": "",
"Starting_Day": "", "Starting_Day": "",
"StartsWith": "", "StartsWith": "",
"StartsWithHelp": "", "StartsWithHelp": "",

View File

@@ -23,6 +23,7 @@
"AiModelHelp": "", "AiModelHelp": "",
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"All": "",
"App": "Приложение", "App": "Приложение",
"Apply": "", "Apply": "",
"Are_You_Sure": "Сигурен ли си?", "Are_You_Sure": "Сигурен ли си?",
@@ -260,6 +261,7 @@
"Ratings": "Рейтинги", "Ratings": "Рейтинги",
"Recently_Viewed": "Наскоро разгледани", "Recently_Viewed": "Наскоро разгледани",
"Recipe": "Рецепта", "Recipe": "Рецепта",
"RecipeStructure": "",
"Recipe_Book": "Книга с рецепти", "Recipe_Book": "Книга с рецепти",
"Recipe_Image": "Изображение на рецептата", "Recipe_Image": "Изображение на рецептата",
"Recipes": "Рецепти", "Recipes": "Рецепти",
@@ -307,6 +309,7 @@
"SpaceMembersHelp": "", "SpaceMembersHelp": "",
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Start": "",
"Starting_Day": "Начален ден от седмицата", "Starting_Day": "Начален ден от седмицата",
"StartsWith": "", "StartsWith": "",
"StartsWithHelp": "", "StartsWithHelp": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Alineació", "Alignment": "Alineació",
"All": "",
"Amount": "Quantitat", "Amount": "Quantitat",
"App": "Aplicació", "App": "Aplicació",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Avaluació", "Ratings": "Avaluació",
"Recently_Viewed": "Vistos recentment", "Recently_Viewed": "Vistos recentment",
"Recipe": "Recepta", "Recipe": "Recepta",
"RecipeStructure": "",
"Recipe_Book": "Llibre de receptes", "Recipe_Book": "Llibre de receptes",
"Recipe_Image": "Imatge de la recepta", "Recipe_Image": "Imatge de la recepta",
"Recipes": "Receptes", "Recipes": "Receptes",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Un administrador de l'espai podria canviar algunes configuracions estètiques i tindrien prioritat sobre la configuració dels usuaris per a aquest espai.", "Space_Cosmetic_Settings": "Un administrador de l'espai podria canviar algunes configuracions estètiques i tindrien prioritat sobre la configuració dels usuaris per a aquest espai.",
"Split_All_Steps": "Dividir totes les files en passos separats.", "Split_All_Steps": "Dividir totes les files en passos separats.",
"Start": "",
"StartDate": "Data d'inici", "StartDate": "Data d'inici",
"Starting_Day": "Dia d'inici de la setmana", "Starting_Day": "Dia d'inici de la setmana",
"StartsWith": "", "StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Zarovnání", "Alignment": "Zarovnání",
"All": "",
"Amount": "Množství", "Amount": "Množství",
"App": "Aplikace", "App": "Aplikace",
"Apply": "", "Apply": "",
@@ -334,6 +335,7 @@
"Ratings": "Hodnocení", "Ratings": "Hodnocení",
"Recently_Viewed": "Naposledy prohlížené", "Recently_Viewed": "Naposledy prohlížené",
"Recipe": "Recept", "Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Kuchařka", "Recipe_Book": "Kuchařka",
"Recipe_Image": "Obrázek k receptu", "Recipe_Image": "Obrázek k receptu",
"Recipes": "Recepty", "Recipes": "Recepty",
@@ -389,6 +391,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.", "Space_Cosmetic_Settings": "Některá kosmetická nastavení mohou měnit správci prostoru a budou mít přednost před nastavením klienta pro daný prostor.",
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.", "Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
"Start": "",
"StartDate": "Počáteční datum", "StartDate": "Počáteční datum",
"Starting_Day": "První den v týdnu", "Starting_Day": "První den v týdnu",
"StartsWith": "", "StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Justering", "Alignment": "Justering",
"All": "",
"Amount": "Mængde", "Amount": "Mængde",
"App": "App", "App": "App",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Bedømmelser", "Ratings": "Bedømmelser",
"Recently_Viewed": "Vist for nylig", "Recently_Viewed": "Vist for nylig",
"Recipe": "Opskrift", "Recipe": "Opskrift",
"RecipeStructure": "",
"Recipe_Book": "Opskriftsbog", "Recipe_Book": "Opskriftsbog",
"Recipe_Image": "Opskriftsbillede", "Recipe_Image": "Opskriftsbillede",
"Recipes": "Opskrifter", "Recipes": "Opskrifter",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Visse kosmetiske indstillinger kan ændres af område-administratorer og vil overskrive klient-indstillinger for pågældende område.", "Space_Cosmetic_Settings": "Visse kosmetiske indstillinger kan ændres af område-administratorer og vil overskrive klient-indstillinger for pågældende område.",
"Split_All_Steps": "Opdel rækker i separate trin.", "Split_All_Steps": "Opdel rækker i separate trin.",
"Start": "",
"StartDate": "Startdato", "StartDate": "Startdato",
"Starting_Day": "Første dag på ugen", "Starting_Day": "Første dag på ugen",
"StartsWith": "", "StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Ευθυγράμμιση", "Alignment": "Ευθυγράμμιση",
"All": "",
"Amount": "Ποσότητα", "Amount": "Ποσότητα",
"App": "Εφαρμογή", "App": "Εφαρμογή",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Βαθμολογίες", "Ratings": "Βαθμολογίες",
"Recently_Viewed": "Προβλήθηκαν πρόσφατα", "Recently_Viewed": "Προβλήθηκαν πρόσφατα",
"Recipe": "Συνταγή", "Recipe": "Συνταγή",
"RecipeStructure": "",
"Recipe_Book": "Βιβλίο συνταγών", "Recipe_Book": "Βιβλίο συνταγών",
"Recipe_Image": "Εικόνα συνταγής", "Recipe_Image": "Εικόνα συνταγής",
"Recipes": "Συνταγές", "Recipes": "Συνταγές",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.", "Space_Cosmetic_Settings": "Ορισμένες ρυθμίσεις εμφάνισης μπορούν να αλλάξουν από τους διαχειριστές του χώρου και θα παρακάμψουν τις ρυθμίσεις πελάτη για αυτόν τον χώρο.",
"Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.", "Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.",
"Start": "",
"StartDate": "Ημερομηνία Έναρξης", "StartDate": "Ημερομηνία Έναρξης",
"Starting_Day": "Πρώτη μέρα της εβδομάδας", "Starting_Day": "Πρώτη μέρα της εβδομάδας",
"StartsWith": "", "StartsWith": "",

View File

@@ -37,6 +37,7 @@
"AiProvider": "AI Provider", "AiProvider": "AI Provider",
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.", "AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
"Alignment": "Alignment", "Alignment": "Alignment",
"All": "All",
"AllRecipes": "All Recipes", "AllRecipes": "All Recipes",
"Amount": "Amount", "Amount": "Amount",
"App": "App", "App": "App",
@@ -149,6 +150,7 @@
"Decimals": "Decimals", "Decimals": "Decimals",
"Default": "Default", "Default": "Default",
"DefaultPage": "Default Page", "DefaultPage": "Default Page",
"DefaultShoppingListHelp": "Default List when this Food is added to the Shoppinglist.",
"Default_Unit": "Default Unit", "Default_Unit": "Default Unit",
"DelayFor": "Delay for {hours} hours", "DelayFor": "Delay for {hours} hours",
"DelayUntil": "Delay Until", "DelayUntil": "Delay Until",
@@ -373,6 +375,7 @@
"NoUnit": "No Unit", "NoUnit": "No Unit",
"No_ID": "ID not found, cannot delete.", "No_ID": "ID not found, cannot delete.",
"No_Results": "No Results", "No_Results": "No Results",
"None": "None",
"NotFound": "Not found", "NotFound": "Not found",
"NotFoundHelp": "The page or object you are looking for could not be found.", "NotFoundHelp": "The page or object you are looking for could not be found.",
"NotInShopping": "{food} is not in your shopping list.", "NotInShopping": "{food} is not in your shopping list.",
@@ -452,6 +455,7 @@
"RecipeBookHelp": "Recipebooks contain recipe book entries or can be automatically populated by using saved search filters. ", "RecipeBookHelp": "Recipebooks contain recipe book entries or can be automatically populated by using saved search filters. ",
"RecipeHelp": "Recipes are the foundation of Tandoor and consist of general information and steps, made up of ingredients, instructions and more. ", "RecipeHelp": "Recipes are the foundation of Tandoor and consist of general information and steps, made up of ingredients, instructions and more. ",
"RecipeStepsHelp": "Ingredients, Instructions and more can be edited in the tab Steps.", "RecipeStepsHelp": "Ingredients, Instructions and more can be edited in the tab Steps.",
"RecipeStructure": "Recipe Structure",
"Recipe_Book": "Recipe Book", "Recipe_Book": "Recipe Book",
"Recipe_Image": "Recipe Image", "Recipe_Image": "Recipe Image",
"Recipes": "Recipes", "Recipes": "Recipes",
@@ -500,14 +504,17 @@
"Share": "Share", "Share": "Share",
"ShopLater": "Shop later", "ShopLater": "Shop later",
"ShopNow": "Shop now", "ShopNow": "Shop now",
"Shopping": "Shopping",
"ShoppingBackgroundSyncWarning": "Bad network, waiting to sync ...", "ShoppingBackgroundSyncWarning": "Bad network, waiting to sync ...",
"ShoppingList": "Shoppinglist",
"ShoppingListEntry": "Shoppinglist Entry", "ShoppingListEntry": "Shoppinglist Entry",
"ShoppingListEntryHelp": "Shopping list entries can be created manually or trough recipes and meal plans.", "ShoppingListEntryHelp": "Shopping list entries can be created manually or trough recipes and meal plans.",
"ShoppingListHelp": "Allows you to put entries on different lists. Can be used for different supermarkets, special offers or events. ",
"ShoppingListRecipe": "Shoppinglist Recipe", "ShoppingListRecipe": "Shoppinglist Recipe",
"Shopping_Categories": "Shopping Categories", "Shopping_Categories": "Shopping Categories",
"Shopping_Category": "Shopping Category", "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_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", "Shopping_list": "Shopping List",
"ShowDelayed": "Show delayed items", "ShowDelayed": "Show delayed items",
"ShowIngredients": "Show Ingredients", "ShowIngredients": "Show Ingredients",
@@ -539,6 +546,7 @@
"Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.", "Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.",
"Split": "Split", "Split": "Split",
"Split_All_Steps": "Split all rows into separate steps.", "Split_All_Steps": "Split all rows into separate steps.",
"Start": "Start",
"StartDate": "Start Date", "StartDate": "Start Date",
"Starting_Day": "Starting day of the week", "Starting_Day": "Starting day of the week",
"StartsWith": "Starts with", "StartsWith": "Starts with",
@@ -608,6 +616,8 @@
"Unrated": "Unrated", "Unrated": "Unrated",
"Up": "Up", "Up": "Up",
"Update": "Update", "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", "Update_Existing_Data": "Update Existing Data",
"Updated": "Updated", "Updated": "Updated",
"UpgradeNow": "Upgrade now", "UpgradeNow": "Upgrade now",

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Tasaus", "Alignment": "Tasaus",
"All": "",
"Amount": "Määrä", "Amount": "Määrä",
"App": "Applikaatio", "App": "Applikaatio",
"Apply": "", "Apply": "",
@@ -326,6 +327,7 @@
"Ratings": "Luokitukset", "Ratings": "Luokitukset",
"Recently_Viewed": "Äskettäin katsotut", "Recently_Viewed": "Äskettäin katsotut",
"Recipe": "Resepti", "Recipe": "Resepti",
"RecipeStructure": "",
"Recipe_Book": "Keittokirja", "Recipe_Book": "Keittokirja",
"Recipe_Image": "Reseptin Kuva", "Recipe_Image": "Reseptin Kuva",
"Recipes": "Reseptit", "Recipes": "Reseptit",
@@ -381,6 +383,7 @@
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.", "Split_All_Steps": "Jaa kaikki rivit erillisiin vaiheisiin.",
"Start": "",
"StartDate": "Aloituspäivä", "StartDate": "Aloituspäivä",
"Starting_Day": "Viikon aloituspäivä", "Starting_Day": "Viikon aloituspäivä",
"StartsWith": "", "StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "יישור", "Alignment": "יישור",
"All": "",
"Amount": "כמות", "Amount": "כמות",
"App": "אפליקציה", "App": "אפליקציה",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "דירוג", "Ratings": "דירוג",
"Recently_Viewed": "נצפו לאחרונה", "Recently_Viewed": "נצפו לאחרונה",
"Recipe": "מתכון", "Recipe": "מתכון",
"RecipeStructure": "",
"Recipe_Book": "ספר מתכון", "Recipe_Book": "ספר מתכון",
"Recipe_Image": "תמונת מתכון", "Recipe_Image": "תמונת מתכון",
"Recipes": "מתכונים", "Recipes": "מתכונים",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.", "Space_Cosmetic_Settings": "חלק מהגדרות הקוסמטיות יכולות להיות מעודכנות על ידי מנהל המרחב וידרסו את הגדרות הקליינט עבור מרחב זה.",
"Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.", "Split_All_Steps": "פצל את כל השורות לצעדים נפרדים.",
"Start": "",
"StartDate": "תאריך התחלה", "StartDate": "תאריך התחלה",
"Starting_Day": "יום תחילת השבוע", "Starting_Day": "יום תחילת השבוע",
"StartsWith": "", "StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Poravnanje", "Alignment": "Poravnanje",
"All": "",
"Amount": "Količina", "Amount": "Količina",
"App": "Aplikacija", "App": "Aplikacija",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Ocjene", "Ratings": "Ocjene",
"Recently_Viewed": "Nedavno pogledano", "Recently_Viewed": "Nedavno pogledano",
"Recipe": "Recept", "Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Knjiga recepata", "Recipe_Book": "Knjiga recepata",
"Recipe_Image": "Slika recepta", "Recipe_Image": "Slika recepta",
"Recipes": "Recepti", "Recipes": "Recepti",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Neke kozmetičke postavke mogu promijeniti administratori prostora i one će poništiti postavke klijenta za taj prostor.", "Space_Cosmetic_Settings": "Neke kozmetičke postavke mogu promijeniti administratori prostora i one će poništiti postavke klijenta za taj prostor.",
"Split_All_Steps": "Podijeli sve retke u zasebne korake.", "Split_All_Steps": "Podijeli sve retke u zasebne korake.",
"Start": "",
"StartDate": "Početni datum", "StartDate": "Početni datum",
"Starting_Day": "Početni dan u tjednu", "Starting_Day": "Početni dan u tjednu",
"StartsWith": "", "StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Igazítás", "Alignment": "Igazítás",
"All": "",
"Amount": "Összeg", "Amount": "Összeg",
"App": "Applikáció", "App": "Applikáció",
"Apply": "", "Apply": "",
@@ -310,6 +311,7 @@
"Ratings": "Értékelések", "Ratings": "Értékelések",
"Recently_Viewed": "Nemrég megtekintett", "Recently_Viewed": "Nemrég megtekintett",
"Recipe": "Recept", "Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Szakácskönyv", "Recipe_Book": "Szakácskönyv",
"Recipe_Image": "Receptkép", "Recipe_Image": "Receptkép",
"Recipes": "Receptek", "Recipes": "Receptek",
@@ -360,6 +362,7 @@
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Ossza fel az összes sort különálló lépésekbe.", "Split_All_Steps": "Ossza fel az összes sort különálló lépésekbe.",
"Start": "",
"StartDate": "Kezdés dátuma", "StartDate": "Kezdés dátuma",
"Starting_Day": "A hét kezdőnapja", "Starting_Day": "A hét kezdőnapja",
"StartsWith": "", "StartsWith": "",

View File

@@ -17,6 +17,7 @@
"AiModelHelp": "", "AiModelHelp": "",
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"All": "",
"Apply": "", "Apply": "",
"Automate": "Ավտոմատացնել", "Automate": "Ավտոմատացնել",
"BatchDeleteConfirm": "", "BatchDeleteConfirm": "",
@@ -143,6 +144,7 @@
"Rating": "", "Rating": "",
"Recently_Viewed": "Վերջերս դիտած", "Recently_Viewed": "Վերջերս դիտած",
"Recipe": "Բաղադրատոմս", "Recipe": "Բաղադրատոմս",
"RecipeStructure": "",
"Recipe_Book": "Բաղադրատոմսերի գիրք", "Recipe_Book": "Բաղադրատոմսերի գիրք",
"Recipe_Image": "Բաղադրատոմսի նկար", "Recipe_Image": "Բաղադրատոմսի նկար",
"Recipes": "Բաղադրատոմսեր", "Recipes": "Բաղադրատոմսեր",
@@ -177,6 +179,7 @@
"SpaceMembersHelp": "", "SpaceMembersHelp": "",
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Start": "",
"StartsWith": "", "StartsWith": "",
"StartsWithHelp": "", "StartsWithHelp": "",
"Step": "", "Step": "",

View File

@@ -25,6 +25,7 @@
"AiModelHelp": "", "AiModelHelp": "",
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"All": "",
"App": "", "App": "",
"Apply": "", "Apply": "",
"Are_You_Sure": "", "Are_You_Sure": "",
@@ -286,6 +287,7 @@
"Ratings": "", "Ratings": "",
"Recently_Viewed": "baru saja dilihat", "Recently_Viewed": "baru saja dilihat",
"Recipe": "", "Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "", "Recipe_Book": "",
"Recipe_Image": "Gambar Resep", "Recipe_Image": "Gambar Resep",
"Recipes": "Resep", "Recipes": "Resep",
@@ -336,6 +338,7 @@
"SpaceMembersHelp": "", "SpaceMembersHelp": "",
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Start": "",
"Starting_Day": "", "Starting_Day": "",
"StartsWith": "", "StartsWith": "",
"StartsWithHelp": "", "StartsWithHelp": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "", "Alignment": "",
"All": "",
"Amount": "", "Amount": "",
"App": "", "App": "",
"Apply": "", "Apply": "",
@@ -336,6 +337,7 @@
"Ratings": "", "Ratings": "",
"Recently_Viewed": "", "Recently_Viewed": "",
"Recipe": "", "Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "", "Recipe_Book": "",
"Recipe_Image": "", "Recipe_Image": "",
"Recipes": "", "Recipes": "",
@@ -392,6 +394,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "", "Space_Cosmetic_Settings": "",
"Split_All_Steps": "", "Split_All_Steps": "",
"Start": "",
"StartDate": "", "StartDate": "",
"Starting_Day": "", "Starting_Day": "",
"StartsWith": "", "StartsWith": "",

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": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "", "Alignment": "",
"All": "",
"Amount": "Suma", "Amount": "Suma",
"App": "", "App": "",
"Apply": "", "Apply": "",
@@ -314,6 +315,7 @@
"Ratings": "", "Ratings": "",
"Recently_Viewed": "Neseniai Žiūrėta", "Recently_Viewed": "Neseniai Žiūrėta",
"Recipe": "", "Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "", "Recipe_Book": "",
"Recipe_Image": "Recepto nuotrauka", "Recipe_Image": "Recepto nuotrauka",
"Recipes": "", "Recipes": "",
@@ -365,6 +367,7 @@
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Split_All_Steps": "", "Split_All_Steps": "",
"Start": "",
"StartDate": "", "StartDate": "",
"Starting_Day": "", "Starting_Day": "",
"StartsWith": "", "StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "", "Alignment": "",
"All": "",
"Amount": "", "Amount": "",
"App": "", "App": "",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "", "Ratings": "",
"Recently_Viewed": "", "Recently_Viewed": "",
"Recipe": "", "Recipe": "",
"RecipeStructure": "",
"Recipe_Book": "", "Recipe_Book": "",
"Recipe_Image": "", "Recipe_Image": "",
"Recipes": "", "Recipes": "",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "", "Space_Cosmetic_Settings": "",
"Split_All_Steps": "", "Split_All_Steps": "",
"Start": "",
"StartDate": "", "StartDate": "",
"Starting_Day": "", "Starting_Day": "",
"StartsWith": "", "StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Justering", "Alignment": "Justering",
"All": "",
"Amount": "Mengde", "Amount": "Mengde",
"App": "App", "App": "App",
"Apply": "", "Apply": "",
@@ -321,6 +322,7 @@
"Ratings": "", "Ratings": "",
"Recently_Viewed": "Nylig vist", "Recently_Viewed": "Nylig vist",
"Recipe": "Oppskrift", "Recipe": "Oppskrift",
"RecipeStructure": "",
"Recipe_Book": "Oppskriftsbok", "Recipe_Book": "Oppskriftsbok",
"Recipe_Image": "Oppskriftsbilde", "Recipe_Image": "Oppskriftsbilde",
"Recipes": "Oppskrift", "Recipes": "Oppskrift",
@@ -375,6 +377,7 @@
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Split_All_Steps": "", "Split_All_Steps": "",
"Start": "",
"StartDate": "Startdato", "StartDate": "Startdato",
"Starting_Day": "Dag uken skal state på", "Starting_Day": "Dag uken skal state på",
"StartsWith": "", "StartsWith": "",

View File

@@ -3,6 +3,7 @@
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.", "AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
"AISettingsHostedHelp": "Je kunt AI-functies inschakelen of beschikbare credits aanpassen door je abonnement te beheren.", "AISettingsHostedHelp": "Je kunt AI-functies inschakelen of beschikbare credits aanpassen door je abonnement te beheren.",
"API": "API", "API": "API",
"APIKey": "API-sleutel",
"API_Browser": "API-browser", "API_Browser": "API-browser",
"API_Documentation": "API-documentatie", "API_Documentation": "API-documentatie",
"AccessTokenHelp": "Toegangssleutels voor de REST API.", "AccessTokenHelp": "Toegangssleutels voor de REST API.",
@@ -55,7 +56,7 @@
"BaseUnit": "Basiseenheid", "BaseUnit": "Basiseenheid",
"BaseUnitHelp": "Standaardeenheid om automatische eenheden om te rekenen", "BaseUnitHelp": "Standaardeenheid om automatische eenheden om te rekenen",
"Basics": "Basisprincipes", "Basics": "Basisprincipes",
"BatchDeleteConfirm": "Wil je alle getoonde items verwijderen? Dit kan niet ongedaan worden gemaakt!", "BatchDeleteConfirm": "Wil je alle getoonde items verwijderen? Dit kan niet ongedaan worden gemaakt! WAARSCHUWING: Het is mogelijk dat hierdoor objecten worden verwijderd die ook elders worden gebruikt. ",
"BatchDeleteHelp": "Als een item niet verwijderd kan worden, wordt het ergens gebruikt. ", "BatchDeleteHelp": "Als een item niet verwijderd kan worden, wordt het ergens gebruikt. ",
"BatchEdit": "Batchbewerking", "BatchEdit": "Batchbewerking",
"BatchEditUpdatingItemsCount": "{count} {type} bewerken", "BatchEditUpdatingItemsCount": "{count} {type} bewerken",
@@ -329,6 +330,7 @@
"Miscellaneous": "Diversen", "Miscellaneous": "Diversen",
"MissingConversion": "Ontbrekende conversie", "MissingConversion": "Ontbrekende conversie",
"MissingProperties": "Ontbrekende eigenschappen", "MissingProperties": "Ontbrekende eigenschappen",
"Model": "Model",
"ModelSelectResultsHelp": "Zoek naar meer resultaten", "ModelSelectResultsHelp": "Zoek naar meer resultaten",
"Monday": "Maandag", "Monday": "Maandag",
"Month": "Maand", "Month": "Maand",
@@ -448,6 +450,7 @@
"RecipeBookHelp": "Receptboeken bevatten receptenboekitems of kunnen automatisch gevuld worden met behulp van opgeslagen zoekfilters. ", "RecipeBookHelp": "Receptboeken bevatten receptenboekitems of kunnen automatisch gevuld worden met behulp van opgeslagen zoekfilters. ",
"RecipeHelp": "Recepten vormen de basis van Tandoor en bestaan uit algemene informatie en stappen, opgebouwd uit ingrediënten, instructies en meer. ", "RecipeHelp": "Recepten vormen de basis van Tandoor en bestaan uit algemene informatie en stappen, opgebouwd uit ingrediënten, instructies en meer. ",
"RecipeStepsHelp": "Ingrediënten, instructies en meer kun je bewerken in het tabblad stappen.", "RecipeStepsHelp": "Ingrediënten, instructies en meer kun je bewerken in het tabblad stappen.",
"RecipeStructure": "Receptstructuur",
"Recipe_Book": "Kookboek", "Recipe_Book": "Kookboek",
"Recipe_Image": "Receptafbeelding", "Recipe_Image": "Receptafbeelding",
"Recipes": "Recepten", "Recipes": "Recepten",
@@ -534,6 +537,7 @@
"Space_Cosmetic_Settings": "Sommige weergave-instellingen kunnen worden geforceerd door de administrator van de ruimte en zullen de persoonlijke instellingen voor die ruimte overschrijven.", "Space_Cosmetic_Settings": "Sommige weergave-instellingen kunnen worden geforceerd door de administrator van de ruimte en zullen de persoonlijke instellingen voor die ruimte overschrijven.",
"Split": "Splitsen", "Split": "Splitsen",
"Split_All_Steps": "Splits alle rijen in aparte stappen.", "Split_All_Steps": "Splits alle rijen in aparte stappen.",
"Start": "Start",
"StartDate": "Startdatum", "StartDate": "Startdatum",
"Starting_Day": "Eerste dag van de week", "Starting_Day": "Eerste dag van de week",
"StartsWith": "Begint met", "StartsWith": "Begint met",
@@ -550,10 +554,12 @@
"Storage": "Externe opslag", "Storage": "Externe opslag",
"StorageHelp": "Extern opgeslagen locaties waar receptenbestanden (afbeelding/pdf) kunnen worden opgeslagen en gesynchroniseerd met Tandoor.", "StorageHelp": "Extern opgeslagen locaties waar receptenbestanden (afbeelding/pdf) kunnen worden opgeslagen en gesynchroniseerd met Tandoor.",
"StoragePasswordTokenHelp": "Het opgeslagen wachtwoord/token wordt nooit weergegeven. Dit wordt alleen gewijzigd als er iets nieuws in het veld wordt ingevoerd. ", "StoragePasswordTokenHelp": "Het opgeslagen wachtwoord/token wordt nooit weergegeven. Dit wordt alleen gewijzigd als er iets nieuws in het veld wordt ingevoerd. ",
"Structured": "Gestructureerd",
"SubstituteOnHand": "Je hebt een alternatief op voorraad.", "SubstituteOnHand": "Je hebt een alternatief op voorraad.",
"Substitutes": "Alternatieven", "Substitutes": "Alternatieven",
"Success": "Succes", "Success": "Succes",
"SuccessClipboard": "Boodschappenlijst is gekopieerd naar klembord", "SuccessClipboard": "Boodschappenlijst is gekopieerd naar klembord",
"Summary": "Samenvatting",
"Sunday": "Zondag", "Sunday": "Zondag",
"Supermarket": "Supermarkt", "Supermarket": "Supermarkt",
"SupermarketCategoriesOnly": "Alleen supermarkt categorieën", "SupermarketCategoriesOnly": "Alleen supermarkt categorieën",
@@ -864,8 +870,12 @@
"warning_duplicate_filter": "Waarschuwing: door technische beperkingen kan het hebben van meerdere filters of dezelfde combinatie (en/of/niet) tot onverwachte resultaten leiden.", "warning_duplicate_filter": "Waarschuwing: door technische beperkingen kan het hebben van meerdere filters of dezelfde combinatie (en/of/niet) tot onverwachte resultaten leiden.",
"warning_feature_beta": "Deze functie zit op dit moment in de BETA (test) fase. Verwacht hier bugs en toekomstige wijzigingen die tot het verlies van data kunnen leiden bij het gebruik.", "warning_feature_beta": "Deze functie zit op dit moment in de BETA (test) fase. Verwacht hier bugs en toekomstige wijzigingen die tot het verlies van data kunnen leiden bij het gebruik.",
"warning_space_delete": "Je kunt jouw ruimte verwijderen inclusief alle recepten, boodschappenlijstjes, maaltijdplannen en alles wat je verder aangemaakt hebt. Dit kan niet ongedaan worden gemaakt! Weet je het zeker?", "warning_space_delete": "Je kunt jouw ruimte verwijderen inclusief alle recepten, boodschappenlijstjes, maaltijdplannen en alles wat je verder aangemaakt hebt. Dit kan niet ongedaan worden gemaakt! Weet je het zeker?",
"Summary": "Samenvatting", "AboutTandoor": "Tandoor is een open source platform om recepten, maaltijdplannen, boodschappenlijstjes en meer te beheren.",
"APIKey": "API-sleutel", "ImportIntoTandoorHelp": "Om dit recept in je eigen Tandoor-collectie te importeren, volg je de volgende stappen.",
"Model": "Model", "SelfHosted": "Zelfgehost",
"Structured": "Gestructureerd" "CreateAccount": "Maak account",
"Shopping": "Boodschappen",
"ShoppingList": "Boodschappenlijst",
"ShoppingListHelp": "Hiermee kun je items op verschillende lijsten plaatsen. Dit kan worden gebruikt voor verschillende supermarkten, speciale aanbiedingen of evenementen. ",
"Finish": "Eind"
} }

View File

@@ -35,6 +35,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Wyrównanie", "Alignment": "Wyrównanie",
"All": "",
"AllRecipes": "Wszystkie przepisy", "AllRecipes": "Wszystkie przepisy",
"Amount": "Ilość", "Amount": "Ilość",
"App": "Aplikacja", "App": "Aplikacja",
@@ -363,6 +364,7 @@
"Ratings": "Oceny", "Ratings": "Oceny",
"Recently_Viewed": "Ostatnio oglądane", "Recently_Viewed": "Ostatnio oglądane",
"Recipe": "Przepis", "Recipe": "Przepis",
"RecipeStructure": "",
"Recipe_Book": "Książka z przepisami", "Recipe_Book": "Książka z przepisami",
"Recipe_Image": "Obrazek dla przepisu", "Recipe_Image": "Obrazek dla przepisu",
"Recipes": "Przepisy", "Recipes": "Przepisy",
@@ -420,6 +422,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.", "Space_Cosmetic_Settings": "Administratorzy przestrzeni mogą zmienić niektóre ustawienia kosmetyczne, które zastąpią ustawienia klienta dla tej przestrzeni.",
"Split_All_Steps": "Traktuj każdy wiersz jako osobne kroki.", "Split_All_Steps": "Traktuj każdy wiersz jako osobne kroki.",
"Start": "",
"StartDate": "Data początkowa", "StartDate": "Data początkowa",
"Starting_Day": "Dzień rozpoczęcia tygodnia", "Starting_Day": "Dzień rozpoczęcia tygodnia",
"StartsWith": "", "StartsWith": "",

View File

@@ -19,6 +19,7 @@
"Added_on": "Adicionado a", "Added_on": "Adicionado a",
"Advanced": "Avançado", "Advanced": "Avançado",
"Alignment": "Alinhamento", "Alignment": "Alinhamento",
"All": "",
"Amount": "Quantidade", "Amount": "Quantidade",
"Apply": "", "Apply": "",
"Auto_Planner": "", "Auto_Planner": "",
@@ -236,6 +237,7 @@
"Ratings": "Avaliações", "Ratings": "Avaliações",
"Recently_Viewed": "Vistos Recentemente", "Recently_Viewed": "Vistos Recentemente",
"Recipe": "Receita", "Recipe": "Receita",
"RecipeStructure": "",
"Recipe_Book": "Livro de Receitas", "Recipe_Book": "Livro de Receitas",
"Recipe_Image": "Imagem da Receita", "Recipe_Image": "Imagem da Receita",
"Recipes": "Receitas", "Recipes": "Receitas",
@@ -271,6 +273,7 @@
"Show_as_header": "Mostrar como cabeçalho", "Show_as_header": "Mostrar como cabeçalho",
"Size": "Tamanho", "Size": "Tamanho",
"Sort_by_new": "Ordenar por mais recente", "Sort_by_new": "Ordenar por mais recente",
"Start": "",
"StartDate": "Data de início", "StartDate": "Data de início",
"Starting_Day": "Dia de início da semana", "Starting_Day": "Dia de início da semana",
"StartsWith": "", "StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiModelHelp": "", "AiModelHelp": "",
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"All": "",
"Amount": "Cantitate", "Amount": "Cantitate",
"App": "Aplicație", "App": "Aplicație",
"Apply": "", "Apply": "",
@@ -298,6 +299,7 @@
"Ratings": "Evaluări", "Ratings": "Evaluări",
"Recently_Viewed": "Vizualizate recent", "Recently_Viewed": "Vizualizate recent",
"Recipe": "Rețetă", "Recipe": "Rețetă",
"RecipeStructure": "",
"Recipe_Book": "Carte de rețete", "Recipe_Book": "Carte de rețete",
"Recipe_Image": "Imagine a rețetei", "Recipe_Image": "Imagine a rețetei",
"Recipes": "Rețete", "Recipes": "Rețete",
@@ -349,6 +351,7 @@
"SpaceName": "", "SpaceName": "",
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Split_All_Steps": "Împărțiți toate rândurile în pași separați.", "Split_All_Steps": "Împărțiți toate rândurile în pași separați.",
"Start": "",
"Starting_Day": "Ziua de început a săptămânii", "Starting_Day": "Ziua de început a săptămânii",
"StartsWith": "", "StartsWith": "",
"StartsWithHelp": "", "StartsWithHelp": "",

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,10 @@
"AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.", "AIImportSubtitle": "Uporabite umetno inteligenco za uvoz slik receptov.",
"AISettingsHostedHelp": "Funkcije umetne inteligence lahko omogočite ali spremenite razpoložljive kredite z upravljanjem naročnine.", "AISettingsHostedHelp": "Funkcije umetne inteligence lahko omogočite ali spremenite razpoložljive kredite z upravljanjem naročnine.",
"API": "API", "API": "API",
"APIKey": "API ključ",
"API_Browser": "API brskalnik", "API_Browser": "API brskalnik",
"API_Documentation": "API dokumentacija", "API_Documentation": "API dokumentacija",
"AboutTandoor": "Tandoor je odprtokodna platforma za upravljanje receptov, načrtov obrokov, nakupovalnih seznamov in še več.",
"AccessTokenHelp": "Dostopni ključi za REST API.", "AccessTokenHelp": "Dostopni ključi za REST API.",
"Access_Token": "Dostopni žeton", "Access_Token": "Dostopni žeton",
"Account": "Račun", "Account": "Račun",
@@ -115,6 +117,7 @@
"Create": "Ustvari", "Create": "Ustvari",
"Create Food": "Ustvari živilo", "Create Food": "Ustvari živilo",
"Create Recipe": "Ustvari recept", "Create Recipe": "Ustvari recept",
"CreateAccount": "Ustvari račun",
"CreateFirstRecipe": "Ustvarite svoj prvi recept z urejevalnikom receptov.", "CreateFirstRecipe": "Ustvarite svoj prvi recept z urejevalnikom receptov.",
"CreateInvitation": "Ustvari povabilo", "CreateInvitation": "Ustvari povabilo",
"Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok", "Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok",
@@ -208,6 +211,7 @@
"Fats": "Maščobe", "Fats": "Maščobe",
"File": "Datoteka", "File": "Datoteka",
"Files": "Datoteke", "Files": "Datoteke",
"Finish": "Končaj",
"FinishedAt": "Končano ob", "FinishedAt": "Končano ob",
"First": "Prvi", "First": "Prvi",
"First_name": "Ime", "First_name": "Ime",
@@ -256,6 +260,7 @@
"ImportAll": "Uvozi vse", "ImportAll": "Uvozi vse",
"ImportFirstRecipe": "Uvozite svoj prvi recept z enega od tisočih spletnih mest ali pa uporabite enega od drugih uvoznikov za uvoz obstoječe zbirke, dokumentov ali seznamov URL-jev.", "ImportFirstRecipe": "Uvozite svoj prvi recept z enega od tisočih spletnih mest ali pa uporabite enega od drugih uvoznikov za uvoz obstoječe zbirke, dokumentov ali seznamov URL-jev.",
"ImportIntoTandoor": "Uvozi v Tandoor", "ImportIntoTandoor": "Uvozi v Tandoor",
"ImportIntoTandoorHelp": "Če želite uvoziti ta recept v svojo zbirko Tandoor, sledite naslednjim korakom.",
"ImportMealPlans": "Uvozi načrte prehrane", "ImportMealPlans": "Uvozi načrte prehrane",
"ImportShoppingList": "Uvozi nakupovalne sezname", "ImportShoppingList": "Uvozi nakupovalne sezname",
"Import_Error": "Med uvozom je prišlo do napake. Za ogled razširite podrobnosti na dnu strani.", "Import_Error": "Med uvozom je prišlo do napake. Za ogled razširite podrobnosti na dnu strani.",
@@ -328,6 +333,7 @@
"Miscellaneous": "Razno", "Miscellaneous": "Razno",
"MissingConversion": "Manjkajoča konverzija", "MissingConversion": "Manjkajoča konverzija",
"MissingProperties": "Manjkajoče lastnosti", "MissingProperties": "Manjkajoče lastnosti",
"Model": "Model",
"ModelSelectResultsHelp": "Išči več rezultatov", "ModelSelectResultsHelp": "Išči več rezultatov",
"Monday": "Ponedeljek", "Monday": "Ponedeljek",
"Month": "Mesec", "Month": "Mesec",
@@ -447,6 +453,7 @@
"RecipeBookHelp": "Knjige receptov vsebujejo vnose v knjige receptov ali pa se lahko samodejno izpolnijo z uporabo shranjenih iskalnih filtrov. ", "RecipeBookHelp": "Knjige receptov vsebujejo vnose v knjige receptov ali pa se lahko samodejno izpolnijo z uporabo shranjenih iskalnih filtrov. ",
"RecipeHelp": "Recepti so osnova Tandoorja in so sestavljeni iz splošnih informacij in korakov, sestavljenih iz sestavin, navodil in še več. ", "RecipeHelp": "Recepti so osnova Tandoorja in so sestavljeni iz splošnih informacij in korakov, sestavljenih iz sestavin, navodil in še več. ",
"RecipeStepsHelp": "Sestavine, navodila in drugo lahko urejate v zavihku Koraki.", "RecipeStepsHelp": "Sestavine, navodila in drugo lahko urejate v zavihku Koraki.",
"RecipeStructure": "Struktura recepta",
"Recipe_Book": "Knjiga receptov", "Recipe_Book": "Knjiga receptov",
"Recipe_Image": "Slika recepta", "Recipe_Image": "Slika recepta",
"Recipes": "Recepti", "Recipes": "Recepti",
@@ -486,6 +493,7 @@
"Select_File": "Izberi datoteko", "Select_File": "Izberi datoteko",
"Selected": "Izbrano", "Selected": "Izbrano",
"SelectedCategories": "Izbrane kategorije", "SelectedCategories": "Izbrane kategorije",
"SelfHosted": "Samostojno gostovanje",
"Serving": "Serviranje", "Serving": "Serviranje",
"Servings": "Porcije", "Servings": "Porcije",
"ServingsText": "Besedilo o porcijah", "ServingsText": "Besedilo o porcijah",
@@ -533,6 +541,7 @@
"Space_Cosmetic_Settings": "Nekatere kozmetične nastavitve lahko spremenijo skrbniki prostora in bodo preglasile nastavitve odjemalca za ta prostor.", "Space_Cosmetic_Settings": "Nekatere kozmetične nastavitve lahko spremenijo skrbniki prostora in bodo preglasile nastavitve odjemalca za ta prostor.",
"Split": "Razdelitev", "Split": "Razdelitev",
"Split_All_Steps": "Vse vrstice razdelite na ločene korake.", "Split_All_Steps": "Vse vrstice razdelite na ločene korake.",
"Start": "Začni",
"StartDate": "Začetni datum", "StartDate": "Začetni datum",
"Starting_Day": "Začetni dan v tednu", "Starting_Day": "Začetni dan v tednu",
"StartsWith": "Začne se s/z", "StartsWith": "Začne se s/z",
@@ -549,10 +558,12 @@
"Storage": "Zunanji pomnilnik", "Storage": "Zunanji pomnilnik",
"StorageHelp": "Zunanje lokacije za shranjevanje, kjer je mogoče shraniti datoteke z recepti (slike/pdf) in sinhronizirati s Tandoorjem.", "StorageHelp": "Zunanje lokacije za shranjevanje, kjer je mogoče shraniti datoteke z recepti (slike/pdf) in sinhronizirati s Tandoorjem.",
"StoragePasswordTokenHelp": "Shranjeno geslo/žeton ne bo nikoli prikazano. Spremeni se le, če v polje vnesete nekaj novega. ", "StoragePasswordTokenHelp": "Shranjeno geslo/žeton ne bo nikoli prikazano. Spremeni se le, če v polje vnesete nekaj novega. ",
"Structured": "Strukturirano",
"SubstituteOnHand": "Pri roki imate nadomestek.", "SubstituteOnHand": "Pri roki imate nadomestek.",
"Substitutes": "Nadomestki", "Substitutes": "Nadomestki",
"Success": "Uspešno", "Success": "Uspešno",
"SuccessClipboard": "Nakupovalni listek je kopiran v odložišče", "SuccessClipboard": "Nakupovalni listek je kopiran v odložišče",
"Summary": "Povzetek",
"Sunday": "Nedelja", "Sunday": "Nedelja",
"Supermarket": "Trgovina", "Supermarket": "Trgovina",
"SupermarketCategoriesOnly": "Prikaži samo trgovinske kategorije", "SupermarketCategoriesOnly": "Prikaži samo trgovinske kategorije",
@@ -862,13 +873,7 @@
"warning_duplicate_filter": "Opozorilo: Zaradi tehničnih omejitev lahko uporaba več filtrov iste kombinacije (in/ali/ne) prinese nepričakovane rezultate.", "warning_duplicate_filter": "Opozorilo: Zaradi tehničnih omejitev lahko uporaba več filtrov iste kombinacije (in/ali/ne) prinese nepričakovane rezultate.",
"warning_feature_beta": "Ta funkcija je trenutno v stanju BETA (testiranje). Pri uporabi te funkcije pričakujte napake in morebitne prelomne spremembe v prihodnosti (morda izgubite podatke, povezane s to funkcijo).", "warning_feature_beta": "Ta funkcija je trenutno v stanju BETA (testiranje). Pri uporabi te funkcije pričakujte napake in morebitne prelomne spremembe v prihodnosti (morda izgubite podatke, povezane s to funkcijo).",
"warning_space_delete": "Izbrišete lahko svoj prostor, vključno z vsemi recepti, nakupovalnimi seznami, načrti obrokov in vsem drugim, kar ste ustvarili. Tega ni mogoče preklicati! Ste prepričani, da želite to storiti?", "warning_space_delete": "Izbrišete lahko svoj prostor, vključno z vsemi recepti, nakupovalnimi seznami, načrti obrokov in vsem drugim, kar ste ustvarili. Tega ni mogoče preklicati! Ste prepričani, da želite to storiti?",
"APIKey": "API ključ", "Shopping": "Nakupovanje",
"Model": "Model", "ShoppingList": "Nakupovalni seznam",
"Structured": "Strukturirano", "ShoppingListHelp": "Omogoča dodajanje vnosov na različne sezname. Uporablja se lahko za različne supermarkete, posebne ponudbe ali dogodke. "
"Summary": "Povzetek",
"AboutTandoor": "Tandoor je odprtokodna platforma za upravljanje receptov, načrtov obrokov, nakupovalnih seznamov in še več.",
"CreateAccount": "Ustvari račun",
"Finish": "Končaj",
"ImportIntoTandoorHelp": "Če želite uvoziti ta recept v svojo zbirko Tandoor, sledite naslednjim korakom.",
"SelfHosted": "Samostojno gostovanje"
} }

View File

@@ -36,6 +36,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Orientering", "Alignment": "Orientering",
"All": "",
"AllRecipes": "Alla recept", "AllRecipes": "Alla recept",
"Amount": "Mängd", "Amount": "Mängd",
"App": "App", "App": "App",
@@ -150,11 +151,13 @@
"DelayFor": "Fördröjning på {hours} timmar", "DelayFor": "Fördröjning på {hours} timmar",
"DelayUntil": "Fördröjning till", "DelayUntil": "Fördröjning till",
"Delete": "Radera", "Delete": "Radera",
"DeleteConfirmQuestion": "Är du säker på att du vill ta bort detta objekt?",
"DeleteShoppingConfirm": "Är du säker på att du vill ta bort all {food} från inköpslistan?", "DeleteShoppingConfirm": "Är du säker på att du vill ta bort all {food} från inköpslistan?",
"DeleteSomething": "", "DeleteSomething": "",
"Delete_All": "Radera alla", "Delete_All": "Radera alla",
"Delete_Food": "Ta bort livsmedel", "Delete_Food": "Ta bort livsmedel",
"Delete_Keyword": "Ta bort nyckelord", "Delete_Keyword": "Ta bort nyckelord",
"Deleted": "Borttagen",
"Description": "Beskrivning", "Description": "Beskrivning",
"Description_Replace": "Ersätt beskrivning", "Description_Replace": "Ersätt beskrivning",
"Disable": "Inaktivera", "Disable": "Inaktivera",

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "Hizalama", "Alignment": "Hizalama",
"All": "",
"Amount": "Miktar", "Amount": "Miktar",
"App": "Uygulama", "App": "Uygulama",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Derecelendirmeler", "Ratings": "Derecelendirmeler",
"Recently_Viewed": "Son Görüntülenen", "Recently_Viewed": "Son Görüntülenen",
"Recipe": "Tarif", "Recipe": "Tarif",
"RecipeStructure": "",
"Recipe_Book": "Yemek Tarifi Kitabı", "Recipe_Book": "Yemek Tarifi Kitabı",
"Recipe_Image": "Tarif Resmi", "Recipe_Image": "Tarif Resmi",
"Recipes": "Tarifler", "Recipes": "Tarifler",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "Bazı kozmetik ayarlar alan yöneticileri tarafından değiştirilebilir ve o alanın istemci ayarlarını geçersiz kılar.", "Space_Cosmetic_Settings": "Bazı kozmetik ayarlar alan yöneticileri tarafından değiştirilebilir ve o alanın istemci ayarlarını geçersiz kılar.",
"Split_All_Steps": "Tüm satırları ayrı adımlara bölün.", "Split_All_Steps": "Tüm satırları ayrı adımlara bölün.",
"Start": "",
"StartDate": "Başlangıç Tarihi", "StartDate": "Başlangıç Tarihi",
"Starting_Day": "Haftanın başlangıç günü", "Starting_Day": "Haftanın başlangıç günü",
"StartsWith": "", "StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiProvider": "", "AiProvider": "",
"AiProviderHelp": "", "AiProviderHelp": "",
"Alignment": "校准", "Alignment": "校准",
"All": "",
"Amount": "数量", "Amount": "数量",
"App": "应用", "App": "应用",
"Apply": "", "Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "等级", "Ratings": "等级",
"Recently_Viewed": "最近浏览", "Recently_Viewed": "最近浏览",
"Recipe": "食谱", "Recipe": "食谱",
"RecipeStructure": "",
"Recipe_Book": "食谱书", "Recipe_Book": "食谱书",
"Recipe_Image": "食谱图像", "Recipe_Image": "食谱图像",
"Recipes": "食谱", "Recipes": "食谱",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "", "SpacePrivateObjectsHelp": "",
"Space_Cosmetic_Settings": "空间管理员可以更改某些装饰设置,并将覆盖该空间的客户端设置。", "Space_Cosmetic_Settings": "空间管理员可以更改某些装饰设置,并将覆盖该空间的客户端设置。",
"Split_All_Steps": "将所有行拆分为单独的步骤。", "Split_All_Steps": "将所有行拆分为单独的步骤。",
"Start": "",
"StartDate": "开始日期", "StartDate": "开始日期",
"Starting_Day": "一周中的第一天", "Starting_Day": "一周中的第一天",
"StartsWith": "", "StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@ models/FdcQueryFoods.ts
models/Food.ts models/Food.ts
models/FoodBatchUpdate.ts models/FoodBatchUpdate.ts
models/FoodInheritField.ts models/FoodInheritField.ts
models/FoodShopping.ts
models/FoodShoppingUpdate.ts models/FoodShoppingUpdate.ts
models/FoodSimple.ts models/FoodSimple.ts
models/GenericModelReference.ts models/GenericModelReference.ts
@@ -94,6 +95,7 @@ models/PaginatedRecipeBookList.ts
models/PaginatedRecipeImportList.ts models/PaginatedRecipeImportList.ts
models/PaginatedRecipeOverviewList.ts models/PaginatedRecipeOverviewList.ts
models/PaginatedShoppingListEntryList.ts models/PaginatedShoppingListEntryList.ts
models/PaginatedShoppingListList.ts
models/PaginatedShoppingListRecipeList.ts models/PaginatedShoppingListRecipeList.ts
models/PaginatedSpaceList.ts models/PaginatedSpaceList.ts
models/PaginatedStepList.ts models/PaginatedStepList.ts
@@ -140,6 +142,7 @@ models/PatchedRecipeBook.ts
models/PatchedRecipeBookEntry.ts models/PatchedRecipeBookEntry.ts
models/PatchedRecipeImport.ts models/PatchedRecipeImport.ts
models/PatchedSearchPreference.ts models/PatchedSearchPreference.ts
models/PatchedShoppingList.ts
models/PatchedShoppingListEntry.ts models/PatchedShoppingListEntry.ts
models/PatchedShoppingListRecipe.ts models/PatchedShoppingListRecipe.ts
models/PatchedSpace.ts models/PatchedSpace.ts
@@ -174,6 +177,7 @@ models/SearchFields.ts
models/SearchPreference.ts models/SearchPreference.ts
models/ServerSettings.ts models/ServerSettings.ts
models/ShareLink.ts models/ShareLink.ts
models/ShoppingList.ts
models/ShoppingListEntry.ts models/ShoppingListEntry.ts
models/ShoppingListEntryBulk.ts models/ShoppingListEntryBulk.ts
models/ShoppingListEntryBulkCreate.ts models/ShoppingListEntryBulkCreate.ts

View File

@@ -85,6 +85,7 @@ import type {
PaginatedRecipeImportList, PaginatedRecipeImportList,
PaginatedRecipeOverviewList, PaginatedRecipeOverviewList,
PaginatedShoppingListEntryList, PaginatedShoppingListEntryList,
PaginatedShoppingListList,
PaginatedShoppingListRecipeList, PaginatedShoppingListRecipeList,
PaginatedSpaceList, PaginatedSpaceList,
PaginatedStepList, PaginatedStepList,
@@ -131,6 +132,7 @@ import type {
PatchedRecipeBookEntry, PatchedRecipeBookEntry,
PatchedRecipeImport, PatchedRecipeImport,
PatchedSearchPreference, PatchedSearchPreference,
PatchedShoppingList,
PatchedShoppingListEntry, PatchedShoppingListEntry,
PatchedShoppingListRecipe, PatchedShoppingListRecipe,
PatchedSpace, PatchedSpace,
@@ -163,6 +165,7 @@ import type {
SearchPreference, SearchPreference,
ServerSettings, ServerSettings,
ShareLink, ShareLink,
ShoppingList,
ShoppingListEntry, ShoppingListEntry,
ShoppingListEntryBulk, ShoppingListEntryBulk,
ShoppingListEntryBulkCreate, ShoppingListEntryBulkCreate,
@@ -324,6 +327,8 @@ import {
PaginatedRecipeOverviewListToJSON, PaginatedRecipeOverviewListToJSON,
PaginatedShoppingListEntryListFromJSON, PaginatedShoppingListEntryListFromJSON,
PaginatedShoppingListEntryListToJSON, PaginatedShoppingListEntryListToJSON,
PaginatedShoppingListListFromJSON,
PaginatedShoppingListListToJSON,
PaginatedShoppingListRecipeListFromJSON, PaginatedShoppingListRecipeListFromJSON,
PaginatedShoppingListRecipeListToJSON, PaginatedShoppingListRecipeListToJSON,
PaginatedSpaceListFromJSON, PaginatedSpaceListFromJSON,
@@ -416,6 +421,8 @@ import {
PatchedRecipeImportToJSON, PatchedRecipeImportToJSON,
PatchedSearchPreferenceFromJSON, PatchedSearchPreferenceFromJSON,
PatchedSearchPreferenceToJSON, PatchedSearchPreferenceToJSON,
PatchedShoppingListFromJSON,
PatchedShoppingListToJSON,
PatchedShoppingListEntryFromJSON, PatchedShoppingListEntryFromJSON,
PatchedShoppingListEntryToJSON, PatchedShoppingListEntryToJSON,
PatchedShoppingListRecipeFromJSON, PatchedShoppingListRecipeFromJSON,
@@ -480,6 +487,8 @@ import {
ServerSettingsToJSON, ServerSettingsToJSON,
ShareLinkFromJSON, ShareLinkFromJSON,
ShareLinkToJSON, ShareLinkToJSON,
ShoppingListFromJSON,
ShoppingListToJSON,
ShoppingListEntryFromJSON, ShoppingListEntryFromJSON,
ShoppingListEntryToJSON, ShoppingListEntryToJSON,
ShoppingListEntryBulkFromJSON, ShoppingListEntryBulkFromJSON,
@@ -1955,6 +1964,21 @@ export interface ApiShareLinkRetrieveRequest {
id: number; id: number;
} }
export interface ApiShoppingListCascadingListRequest {
id: number;
cache?: boolean;
page?: number;
pageSize?: number;
}
export interface ApiShoppingListCreateRequest {
shoppingList?: ShoppingList;
}
export interface ApiShoppingListDestroyRequest {
id: number;
}
export interface ApiShoppingListEntryBulkCreateRequest { export interface ApiShoppingListEntryBulkCreateRequest {
shoppingListEntryBulk: Omit<ShoppingListEntryBulk, 'timestamp'>; shoppingListEntryBulk: Omit<ShoppingListEntryBulk, 'timestamp'>;
} }
@@ -1988,6 +2012,30 @@ export interface ApiShoppingListEntryUpdateRequest {
shoppingListEntry: Omit<ShoppingListEntry, 'listRecipeData'|'createdBy'|'createdAt'|'updatedAt'>; shoppingListEntry: Omit<ShoppingListEntry, 'listRecipeData'|'createdBy'|'createdAt'|'updatedAt'>;
} }
export interface ApiShoppingListListRequest {
page?: number;
pageSize?: number;
}
export interface ApiShoppingListNullingListRequest {
id: number;
cache?: boolean;
page?: number;
pageSize?: number;
}
export interface ApiShoppingListPartialUpdateRequest {
id: number;
patchedShoppingList?: PatchedShoppingList;
}
export interface ApiShoppingListProtectingListRequest {
id: number;
cache?: boolean;
page?: number;
pageSize?: number;
}
export interface ApiShoppingListRecipeBulkCreateEntriesCreateRequest { export interface ApiShoppingListRecipeBulkCreateEntriesCreateRequest {
id: number; id: number;
shoppingListEntryBulkCreate: ShoppingListEntryBulkCreate; shoppingListEntryBulkCreate: ShoppingListEntryBulkCreate;
@@ -2021,6 +2069,15 @@ export interface ApiShoppingListRecipeUpdateRequest {
shoppingListRecipe: Omit<ShoppingListRecipe, 'recipeData'|'mealPlanData'|'createdBy'>; shoppingListRecipe: Omit<ShoppingListRecipe, 'recipeData'|'mealPlanData'|'createdBy'>;
} }
export interface ApiShoppingListRetrieveRequest {
id: number;
}
export interface ApiShoppingListUpdateRequest {
id: number;
shoppingList?: ShoppingList;
}
export interface ApiSpaceCreateRequest { export interface ApiSpaceCreateRequest {
space?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>; space?: Omit<Space, 'createdBy'|'createdAt'|'maxRecipes'|'maxFileStorageMb'|'maxUsers'|'allowSharing'|'demo'|'userCount'|'recipeCount'|'fileSizeMb'|'aiMonthlyCreditsUsed'>;
} }
@@ -14551,6 +14608,124 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value(); return await response.value();
} }
/**
* get a paginated list of objects that will be cascaded (deleted) when deleting the selected object
*/
async apiShoppingListCascadingListRaw(requestParameters: ApiShoppingListCascadingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedGenericModelReferenceList>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListCascadingList().'
);
}
const queryParameters: any = {};
if (requestParameters['cache'] != null) {
queryParameters['cache'] = requestParameters['cache'];
}
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/cascading/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedGenericModelReferenceListFromJSON(jsonValue));
}
/**
* get a paginated list of objects that will be cascaded (deleted) when deleting the selected object
*/
async apiShoppingListCascadingList(requestParameters: ApiShoppingListCascadingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedGenericModelReferenceList> {
const response = await this.apiShoppingListCascadingListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListCreateRaw(requestParameters: ApiShoppingListCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ShoppingList>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: ShoppingListToJSON(requestParameters['shoppingList']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => ShoppingListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListCreate(requestParameters: ApiShoppingListCreateRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ShoppingList> {
const response = await this.apiShoppingListCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListDestroyRaw(requestParameters: ApiShoppingListDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListDestroy().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'DELETE',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListDestroy(requestParameters: ApiShoppingListDestroyRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.apiShoppingListDestroyRaw(requestParameters, initOverrides);
}
/** /**
* individual entries of a shopping list automatically filtered to only contain unchecked items that are not older than the shopping recent days setting to not bloat endpoint * individual entries of a shopping list automatically filtered to only contain unchecked items that are not older than the shopping recent days setting to not bloat endpoint
*/ */
@@ -14837,6 +15012,182 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value(); return await response.value();
} }
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListListRaw(requestParameters: ApiShoppingListListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedShoppingListList>> {
const queryParameters: any = {};
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedShoppingListListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListList(requestParameters: ApiShoppingListListRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedShoppingListList> {
const response = await this.apiShoppingListListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* get a paginated list of objects where the selected object will be removed whe its deleted
*/
async apiShoppingListNullingListRaw(requestParameters: ApiShoppingListNullingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedGenericModelReferenceList>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListNullingList().'
);
}
const queryParameters: any = {};
if (requestParameters['cache'] != null) {
queryParameters['cache'] = requestParameters['cache'];
}
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/nulling/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedGenericModelReferenceListFromJSON(jsonValue));
}
/**
* get a paginated list of objects where the selected object will be removed whe its deleted
*/
async apiShoppingListNullingList(requestParameters: ApiShoppingListNullingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedGenericModelReferenceList> {
const response = await this.apiShoppingListNullingListRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListPartialUpdateRaw(requestParameters: ApiShoppingListPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ShoppingList>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListPartialUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PATCH',
headers: headerParameters,
query: queryParameters,
body: PatchedShoppingListToJSON(requestParameters['patchedShoppingList']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => ShoppingListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListPartialUpdate(requestParameters: ApiShoppingListPartialUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ShoppingList> {
const response = await this.apiShoppingListPartialUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* get a paginated list of objects that are protecting the selected object form being deleted
*/
async apiShoppingListProtectingListRaw(requestParameters: ApiShoppingListProtectingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedGenericModelReferenceList>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListProtectingList().'
);
}
const queryParameters: any = {};
if (requestParameters['cache'] != null) {
queryParameters['cache'] = requestParameters['cache'];
}
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
if (requestParameters['pageSize'] != null) {
queryParameters['page_size'] = requestParameters['pageSize'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/protecting/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => PaginatedGenericModelReferenceListFromJSON(jsonValue));
}
/**
* get a paginated list of objects that are protecting the selected object form being deleted
*/
async apiShoppingListProtectingList(requestParameters: ApiShoppingListProtectingListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<PaginatedGenericModelReferenceList> {
const response = await this.apiShoppingListProtectingListRaw(requestParameters, initOverrides);
return await response.value();
}
/** /**
* logs request counts to redis cache total/per user/ * logs request counts to redis cache total/per user/
*/ */
@@ -15126,6 +15477,83 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value(); return await response.value();
} }
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListRetrieveRaw(requestParameters: ApiShoppingListRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ShoppingList>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListRetrieve().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => ShoppingListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListRetrieve(requestParameters: ApiShoppingListRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ShoppingList> {
const response = await this.apiShoppingListRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListUpdateRaw(requestParameters: ApiShoppingListUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ShoppingList>> {
if (requestParameters['id'] == null) {
throw new runtime.RequiredError(
'id',
'Required parameter "id" was null or undefined when calling apiShoppingListUpdate().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/shopping-list/{id}/`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))),
method: 'PUT',
headers: headerParameters,
query: queryParameters,
body: ShoppingListToJSON(requestParameters['shoppingList']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => ShoppingListFromJSON(jsonValue));
}
/**
* logs request counts to redis cache total/per user/
*/
async apiShoppingListUpdate(requestParameters: ApiShoppingListUpdateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ShoppingList> {
const response = await this.apiShoppingListUpdateRaw(requestParameters, initOverrides);
return await response.value();
}
/** /**
* logs request counts to redis cache total/per user/ * logs request counts to redis cache total/per user/
*/ */

View File

@@ -13,6 +13,12 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { SupermarketCategory } from './SupermarketCategory'; import type { SupermarketCategory } from './SupermarketCategory';
import { import {
SupermarketCategoryFromJSON, SupermarketCategoryFromJSON,
@@ -235,6 +241,12 @@ export interface Food {
* @memberof Food * @memberof Food
*/ */
openDataSlug?: string; 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'], 'substituteOnhand': json['substitute_onhand'],
'childInheritFields': json['child_inherit_fields'] == null ? undefined : ((json['child_inherit_fields'] as Array<any>).map(FoodInheritFieldFromJSON)), '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'], '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'], 'substitute_children': value['substituteChildren'],
'child_inherit_fields': value['childInheritFields'] == null ? undefined : ((value['childInheritFields'] as Array<any>).map(FoodInheritFieldToJSON)), 'child_inherit_fields': value['childInheritFields'] == null ? undefined : ((value['childInheritFields'] as Array<any>).map(FoodInheritFieldToJSON)),
'open_data_slug': value['openDataSlug'], '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

@@ -0,0 +1,101 @@
/* 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';
/**
*
* @export
* @interface PaginatedShoppingListList
*/
export interface PaginatedShoppingListList {
/**
*
* @type {number}
* @memberof PaginatedShoppingListList
*/
count: number;
/**
*
* @type {string}
* @memberof PaginatedShoppingListList
*/
next?: string;
/**
*
* @type {string}
* @memberof PaginatedShoppingListList
*/
previous?: string;
/**
*
* @type {Array<ShoppingList>}
* @memberof PaginatedShoppingListList
*/
results: Array<ShoppingList>;
/**
*
* @type {Date}
* @memberof PaginatedShoppingListList
*/
timestamp?: Date;
}
/**
* Check if a given object implements the PaginatedShoppingListList interface.
*/
export function instanceOfPaginatedShoppingListList(value: object): value is PaginatedShoppingListList {
if (!('count' in value) || value['count'] === undefined) return false;
if (!('results' in value) || value['results'] === undefined) return false;
return true;
}
export function PaginatedShoppingListListFromJSON(json: any): PaginatedShoppingListList {
return PaginatedShoppingListListFromJSONTyped(json, false);
}
export function PaginatedShoppingListListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PaginatedShoppingListList {
if (json == null) {
return json;
}
return {
'count': json['count'],
'next': json['next'] == null ? undefined : json['next'],
'previous': json['previous'] == null ? undefined : json['previous'],
'results': ((json['results'] as Array<any>).map(ShoppingListFromJSON)),
'timestamp': json['timestamp'] == null ? undefined : (new Date(json['timestamp'])),
};
}
export function PaginatedShoppingListListToJSON(value?: PaginatedShoppingListList | null): any {
if (value == null) {
return value;
}
return {
'count': value['count'],
'next': value['next'],
'previous': value['previous'],
'results': ((value['results'] as Array<any>).map(ShoppingListToJSON)),
'timestamp': value['timestamp'] == null ? undefined : ((value['timestamp']).toISOString()),
};
}

View File

@@ -13,6 +13,12 @@
*/ */
import { mapValues } from '../runtime'; import { mapValues } from '../runtime';
import type { ShoppingList } from './ShoppingList';
import {
ShoppingListFromJSON,
ShoppingListFromJSONTyped,
ShoppingListToJSON,
} from './ShoppingList';
import type { SupermarketCategory } from './SupermarketCategory'; import type { SupermarketCategory } from './SupermarketCategory';
import { import {
SupermarketCategoryFromJSON, SupermarketCategoryFromJSON,
@@ -235,6 +241,12 @@ export interface PatchedFood {
* @memberof PatchedFood * @memberof PatchedFood
*/ */
openDataSlug?: string; 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'], '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)), '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'], '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'], 'substitute_children': value['substituteChildren'],
'child_inherit_fields': value['childInheritFields'] == null ? undefined : ((value['childInheritFields'] as Array<any>).map(FoodInheritFieldToJSON)), 'child_inherit_fields': value['childInheritFields'] == null ? undefined : ((value['childInheritFields'] as Array<any>).map(FoodInheritFieldToJSON)),
'open_data_slug': value['openDataSlug'], 'open_data_slug': value['openDataSlug'],
'shopping_lists': value['shoppingLists'] == null ? undefined : ((value['shoppingLists'] as Array<any>).map(ShoppingListToJSON)),
}; };
} }

View File

@@ -0,0 +1,84 @@
/* 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';
/**
* Adds nested create feature
* @export
* @interface PatchedShoppingList
*/
export interface PatchedShoppingList {
/**
*
* @type {number}
* @memberof PatchedShoppingList
*/
id?: number;
/**
*
* @type {string}
* @memberof PatchedShoppingList
*/
name?: string;
/**
*
* @type {string}
* @memberof PatchedShoppingList
*/
description?: string;
/**
*
* @type {string}
* @memberof PatchedShoppingList
*/
color?: string;
}
/**
* Check if a given object implements the PatchedShoppingList interface.
*/
export function instanceOfPatchedShoppingList(value: object): value is PatchedShoppingList {
return true;
}
export function PatchedShoppingListFromJSON(json: any): PatchedShoppingList {
return PatchedShoppingListFromJSONTyped(json, false);
}
export function PatchedShoppingListFromJSONTyped(json: any, ignoreDiscriminator: boolean): PatchedShoppingList {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'color': json['color'] == null ? undefined : json['color'],
};
}
export function PatchedShoppingListToJSON(value?: PatchedShoppingList | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'description': value['description'],
'color': value['color'],
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
/* 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';
/**
* Adds nested create feature
* @export
* @interface ShoppingList
*/
export interface ShoppingList {
/**
*
* @type {number}
* @memberof ShoppingList
*/
id?: number;
/**
*
* @type {string}
* @memberof ShoppingList
*/
name?: string;
/**
*
* @type {string}
* @memberof ShoppingList
*/
description?: string;
/**
*
* @type {string}
* @memberof ShoppingList
*/
color?: string;
}
/**
* Check if a given object implements the ShoppingList interface.
*/
export function instanceOfShoppingList(value: object): value is ShoppingList {
return true;
}
export function ShoppingListFromJSON(json: any): ShoppingList {
return ShoppingListFromJSONTyped(json, false);
}
export function ShoppingListFromJSONTyped(json: any, ignoreDiscriminator: boolean): ShoppingList {
if (json == null) {
return json;
}
return {
'id': json['id'] == null ? undefined : json['id'],
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'color': json['color'] == null ? undefined : json['color'],
};
}
export function ShoppingListToJSON(value?: ShoppingList | null): any {
if (value == null) {
return value;
}
return {
'id': value['id'],
'name': value['name'],
'description': value['description'],
'color': value['color'],
};
}

View File

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

View File

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

View File

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

View File

@@ -200,6 +200,12 @@ export interface UserPreference {
* @memberof UserPreference * @memberof UserPreference
*/ */
csvPrefix?: string; csvPrefix?: string;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
shoppingUpdateFoodLists?: boolean;
/** /**
* *
* @type {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'], 'shoppingRecentDays': json['shopping_recent_days'] == null ? undefined : json['shopping_recent_days'],
'csvDelim': json['csv_delim'] == null ? undefined : json['csv_delim'], 'csvDelim': json['csv_delim'] == null ? undefined : json['csv_delim'],
'csvPrefix': json['csv_prefix'] == null ? undefined : json['csv_prefix'], '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'], 'filterToSupermarket': json['filter_to_supermarket'] == null ? undefined : json['filter_to_supermarket'],
'shoppingAddOnhand': json['shopping_add_onhand'] == null ? undefined : json['shopping_add_onhand'], 'shoppingAddOnhand': json['shopping_add_onhand'] == null ? undefined : json['shopping_add_onhand'],
'leftHanded': json['left_handed'] == null ? undefined : json['left_handed'], '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'], 'shopping_recent_days': value['shoppingRecentDays'],
'csv_delim': value['csvDelim'], 'csv_delim': value['csvDelim'],
'csv_prefix': value['csvPrefix'], 'csv_prefix': value['csvPrefix'],
'shopping_update_food_lists': value['shoppingUpdateFoodLists'],
'filter_to_supermarket': value['filterToSupermarket'], 'filter_to_supermarket': value['filterToSupermarket'],
'shopping_add_onhand': value['shoppingAddOnhand'], 'shopping_add_onhand': value['shoppingAddOnhand'],
'left_handed': value['leftHanded'], 'left_handed': value['leftHanded'],

View File

@@ -29,6 +29,7 @@ export * from './FdcQueryFoods';
export * from './Food'; export * from './Food';
export * from './FoodBatchUpdate'; export * from './FoodBatchUpdate';
export * from './FoodInheritField'; export * from './FoodInheritField';
export * from './FoodShopping';
export * from './FoodShoppingUpdate'; export * from './FoodShoppingUpdate';
export * from './FoodSimple'; export * from './FoodSimple';
export * from './GenericModelReference'; export * from './GenericModelReference';
@@ -92,6 +93,7 @@ export * from './PaginatedRecipeBookList';
export * from './PaginatedRecipeImportList'; export * from './PaginatedRecipeImportList';
export * from './PaginatedRecipeOverviewList'; export * from './PaginatedRecipeOverviewList';
export * from './PaginatedShoppingListEntryList'; export * from './PaginatedShoppingListEntryList';
export * from './PaginatedShoppingListList';
export * from './PaginatedShoppingListRecipeList'; export * from './PaginatedShoppingListRecipeList';
export * from './PaginatedSpaceList'; export * from './PaginatedSpaceList';
export * from './PaginatedStepList'; export * from './PaginatedStepList';
@@ -138,6 +140,7 @@ export * from './PatchedRecipeBook';
export * from './PatchedRecipeBookEntry'; export * from './PatchedRecipeBookEntry';
export * from './PatchedRecipeImport'; export * from './PatchedRecipeImport';
export * from './PatchedSearchPreference'; export * from './PatchedSearchPreference';
export * from './PatchedShoppingList';
export * from './PatchedShoppingListEntry'; export * from './PatchedShoppingListEntry';
export * from './PatchedShoppingListRecipe'; export * from './PatchedShoppingListRecipe';
export * from './PatchedSpace'; export * from './PatchedSpace';
@@ -172,6 +175,7 @@ export * from './SearchFields';
export * from './SearchPreference'; export * from './SearchPreference';
export * from './ServerSettings'; export * from './ServerSettings';
export * from './ShareLink'; export * from './ShareLink';
export * from './ShoppingList';
export * from './ShoppingListEntry'; export * from './ShoppingListEntry';
export * from './ShoppingListEntryBulk'; export * from './ShoppingListEntryBulk';
export * from './ShoppingListEntryBulkCreate'; export * from './ShoppingListEntryBulkCreate';

View File

@@ -31,6 +31,7 @@
</v-row> </v-row>
<v-row dense> <v-row dense>
<database-model-col model="Supermarket"></database-model-col> <database-model-col model="Supermarket"></database-model-col>
<database-model-col model="ShoppingList"></database-model-col>
<database-model-col model="SupermarketCategory"></database-model-col> <database-model-col model="SupermarketCategory"></database-model-col>
<database-model-col model="MealType"></database-model-col> <database-model-col model="MealType"></database-model-col>
</v-row> </v-row>

View File

@@ -95,6 +95,9 @@
<v-chip label v-if="item.id == useUserPreferenceStore().activeSpace.id!" color="success">{{ $t('Active') }}</v-chip> <v-chip label v-if="item.id == useUserPreferenceStore().activeSpace.id!" color="success">{{ $t('Active') }}</v-chip>
<v-chip label v-else color="info" @click="useUserPreferenceStore().switchSpace(item)">{{ $t('Select') }}</v-chip> <v-chip label v-else color="info" @click="useUserPreferenceStore().switchSpace(item)">{{ $t('Select') }}</v-chip>
</template> </template>
<template v-slot:item.color="{ item }">
<v-chip label :color="item.color">{{ item.color }}</v-chip>
</template>
<template v-slot:item.action="{ item }"> <template v-slot:item.action="{ item }">
<v-btn class="float-right" icon="$menu" variant="plain"> <v-btn class="float-right" icon="$menu" variant="plain">
<v-icon icon="$menu"></v-icon> <v-icon icon="$menu"></v-icon>

View File

@@ -606,7 +606,7 @@ function importFromUrlList() {
setTimeout(importFromUrlList, 500) setTimeout(importFromUrlList, 500)
}) })
}).catch(err => { }).catch(err => {
setTimeout(importFromUrlList, 500)
}).finally(() => { }).finally(() => {
loading.value = false loading.value = false
}) })

View File

@@ -2,7 +2,8 @@
<v-container :class="{'ps-0 pe-0 pt-0': mobile}"> <v-container :class="{'ps-0 pe-0 pt-0': mobile}">
<v-defaults-provider :defaults="(useUserPreferenceStore().isPrintMode ? {VCard: {variant: 'flat'}} : {})"> <v-defaults-provider :defaults="(useUserPreferenceStore().isPrintMode ? {VCard: {variant: 'flat'}} : {})">
<recipe-view v-model="recipe"></recipe-view>
<recipe-view v-model="recipe" :servings="servings"></recipe-view>
<div class="mt-2" v-if="isShared && Object.keys(recipe).length > 0"> <div class="mt-2" v-if="isShared && Object.keys(recipe).length > 0">
<import-tandoor-dialog></import-tandoor-dialog> <import-tandoor-dialog></import-tandoor-dialog>
@@ -35,6 +36,13 @@ const isShared = computed(() => {
return params.share && typeof params.share == "string" return params.share && typeof params.share == "string"
}) })
const servings = computed(() => {
const value = params.servings
if (!value) return undefined
const parsed = parseInt(value as string, 10)
return parsed > 0 ? parsed : undefined
})
const recipe = ref({} as Recipe) const recipe = ref({} as Recipe)
watch(() => props.id, () => { watch(() => props.id, () => {

View File

@@ -184,7 +184,7 @@ import RecipeCard from "@/components/display/RecipeCard.vue";
import {useDisplay} from "vuetify"; import {useDisplay} from "vuetify";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {useRouteQuery} from "@vueuse/router"; import {useRouteQuery} from "@vueuse/router";
import {routeQueryDateTransformer, stringToBool, toNumberArray} from "@/utils/utils"; import {numberOrUndefinedTransformer, routeQueryDateTransformer, stringToBool, toNumberArray} from "@/utils/utils";
import RandomIcon from "@/components/display/RandomIcon.vue"; import RandomIcon from "@/components/display/RandomIcon.vue";
import {VSelect, VTextField, VNumberInput} from "vuetify/components"; import {VSelect, VTextField, VNumberInput} from "vuetify/components";
import RatingField from "@/components/inputs/RatingField.vue"; import RatingField from "@/components/inputs/RatingField.vue";
@@ -759,27 +759,30 @@ const filters = ref({
label: `${t('Rating')} (${t('exact')})`, label: `${t('Rating')} (${t('exact')})`,
hint: '', hint: '',
enabled: false, enabled: false,
clearable: true,
default: undefined, default: undefined,
is: RatingField, is: RatingField,
modelValue: useRouteQuery('rating', undefined, {transform: Number}), modelValue: useRouteQuery('rating', undefined, {transform: numberOrUndefinedTransformer}),
}, },
ratingGte: { ratingGte: {
id: 'ratingGte', id: 'ratingGte',
label: `${t('Rating')} (>=)`, label: `${t('Rating')} (>=)`,
hint: '', hint: '',
enabled: false, enabled: false,
clearable: true,
default: undefined, default: undefined,
is: RatingField, is: RatingField,
modelValue: useRouteQuery('ratingGte', undefined, {transform: Number}), modelValue: useRouteQuery('ratingGte', undefined, {transform: numberOrUndefinedTransformer}),
}, },
ratingLte: { ratingLte: {
id: 'ratingLte', id: 'ratingLte',
label: `${t('Rating')} (<=)`, label: `${t('Rating')} (<=)`,
hint: '', hint: '',
enabled: false, enabled: false,
clearable: true,
default: undefined, default: undefined,
is: RatingField, is: RatingField,
modelValue: useRouteQuery('ratingLte', undefined, {transform: Number}), modelValue: useRouteQuery('ratingLte', undefined, {transform: numberOrUndefinedTransformer}),
}, },
timescooked: { timescooked: {
id: 'timescooked', id: 'timescooked',
@@ -787,26 +790,29 @@ const filters = ref({
hint: 'Recipes that were cooked at least X times', hint: 'Recipes that were cooked at least X times',
enabled: false, enabled: false,
default: undefined, default: undefined,
clearable: true,
is: VNumberInput, is: VNumberInput,
modelValue: useRouteQuery('timescooked', undefined, {transform: Number}), modelValue: useRouteQuery('timescooked', undefined, {transform: numberOrUndefinedTransformer}),
}, },
timescookedGte: { timescookedGte: {
id: 'timescookedGte', id: 'timescookedGte',
label: `${t('times_cooked')} (>=)`, label: `${t('times_cooked')} (>=)`,
hint: '', hint: '',
enabled: false, enabled: false,
clearable: true,
default: undefined, default: undefined,
is: VNumberInput, is: VNumberInput,
modelValue: useRouteQuery('timescookedGte', undefined, {transform: Number}), modelValue: useRouteQuery('timescookedGte', undefined, {transform: numberOrUndefinedTransformer}),
}, },
timescookedLte: { timescookedLte: {
id: 'timescookedLte', id: 'timescookedLte',
label: `${t('times_cooked')} (<=)`, label: `${t('times_cooked')} (<=)`,
hint: '', hint: '',
enabled: false, enabled: false,
clearable: true,
default: undefined, default: undefined,
is: VNumberInput, is: VNumberInput,
modelValue: useRouteQuery('timescookedLte', undefined, {transform: Number}), modelValue: useRouteQuery('timescookedLte', undefined, {transform: numberOrUndefinedTransformer}),
}, },
makenow: { makenow: {
id: 'makenow', id: 'makenow',

View File

@@ -1,6 +1,6 @@
import {acceptHMRUpdate, defineStore} from "pinia" import {acceptHMRUpdate, defineStore} from "pinia"
import {ApiApi, ApiShoppingListEntryListRequest, Food, Recipe, ShoppingListEntry, ShoppingListEntryBulk, ShoppingListRecipe, Supermarket, SupermarketCategory} from "@/openapi"; 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 { import {
IShoppingExportEntry, IShoppingExportEntry,
IShoppingList, IShoppingList,
@@ -25,14 +25,6 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let supermarketCategories = ref([] as SupermarketCategory[]) let supermarketCategories = ref([] as SupermarketCategory[])
let supermarkets = ref([] as Supermarket[]) let supermarkets = ref([] as Supermarket[])
let stats = ref({
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats)
// internal // internal
let currentlyUpdating = ref(false) let currentlyUpdating = ref(false)
let initialized = ref(false) let initialized = ref(false)
@@ -41,28 +33,25 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let autoSyncHasFocus = ref(true) let autoSyncHasFocus = ref(true)
let autoSyncTimeoutId = ref(0) let autoSyncTimeoutId = ref(0)
let undoStack = ref([] as ShoppingOperationHistoryEntry[]) let undoStack = shallowRef([] as ShoppingOperationHistoryEntry[])
let queueTimeoutId = ref(-1) 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 * build a multi-level data structure ready for display from shopping list entries
* group by selected grouping key * group by selected grouping key
*/ */
const getEntriesByGroup = computed(() => { function updateEntriesStructure() {
stats.value = {
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats
let structure = {} as IShoppingList let structure = {} as IShoppingList
structure.categories = new Map<string, IShoppingListCategory> structure.categories = new Map<string, IShoppingListCategory>
if (useUserPreferenceStore().deviceSettings.shopping_selected_grouping === ShoppingGroupingOptions.CATEGORY && useUserPreferenceStore().deviceSettings.shopping_selected_supermarket != null) { const deviceSettings = useUserPreferenceStore().deviceSettings
useUserPreferenceStore().deviceSettings.shopping_selected_supermarket.categoryToSupermarket.forEach(cTS => {
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) 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 // build structure
entries.value.forEach(shoppingListEntry => { entries.value.forEach(shoppingListEntry => {
structure = updateEntryInStructure(structure, shoppingListEntry) if (isEntryVisible(shoppingListEntry, deviceSettings)) {
}) 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
}) })
// ordering // ordering
@@ -121,10 +73,27 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
} }
structure.categories.forEach(category => { 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() { function getFlatEntries() {
let items: IShoppingExportEntry[] = [] let items: IShoppingExportEntry[] = []
getEntriesByGroup.value.forEach(shoppingListEntry => { entriesByGroup.value.forEach(shoppingListEntry => {
shoppingListEntry.foods.forEach(food => { shoppingListEntry.foods.forEach(food => {
food.entries.forEach(entry => { food.entries.forEach(entry => {
items.push({ items.push({
@@ -175,7 +144,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
function hasFailedItems() { function hasFailedItems() {
for (let i in itemCheckSyncQueue.value) { for (let i in itemCheckSyncQueue.value) {
if (itemCheckSyncQueue.value[i]['status'] === 'syncing_failed_before' || itemCheckSyncQueue.value[i]['status'] === 'waiting_failed_before') { if (itemCheckSyncQueue.value[i]['status'] === 'syncing_failed_before' || itemCheckSyncQueue.value[i]['status'] === 'waiting_failed_before') {
return true return !syncQueueRunning.value
} }
} }
return false return false
@@ -197,6 +166,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
} else { } else {
// only clear local entries when not given a meal plan to not accidentally filter the shopping list // only clear local entries when not given a meal plan to not accidentally filter the shopping list
entries.value = new Map<number, ShoppingListEntry> entries.value = new Map<number, ShoppingListEntry>
initialized.value = false
} }
recLoadShoppingListEntries(requestParameters) recLoadShoppingListEntries(requestParameters)
@@ -221,17 +191,30 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
*/ */
function recLoadShoppingListEntries(requestParameters: ApiShoppingListEntryListRequest) { function recLoadShoppingListEntries(requestParameters: ApiShoppingListEntryListRequest) {
let api = new ApiApi() 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) => { r.results.forEach((e) => {
entries.value.set(e.id!, e) newMap.set(e.id!, e)
}) })
if (r.next) { // bulk assign to avoid unnecessary reactivity updates
requestParameters.page = requestParameters.page + 1 entries.value = new Map([...entries.value, ...newMap])
recLoadShoppingListEntries(requestParameters)
} else { if (requestParameters.page == 1) {
currentlyUpdating.value = false if (r.next) {
initialized.value = true 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) => { }).catch((err) => {
currentlyUpdating.value = false currentlyUpdating.value = false
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -251,6 +234,9 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
r.results.forEach((e) => { r.results.forEach((e) => {
entries.value.set(e.id!, e) entries.value.set(e.id!, e)
}) })
if(r.results.length > 0){
updateEntriesStructure()
}
currentlyUpdating.value = false currentlyUpdating.value = false
}).catch((err: any) => { }).catch((err: any) => {
currentlyUpdating.value = false currentlyUpdating.value = false
@@ -267,6 +253,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
const api = new ApiApi() const api = new ApiApi()
return api.apiShoppingListEntryCreate({shoppingListEntry: object}).then((r) => { return api.apiShoppingListEntryCreate({shoppingListEntry: object}).then((r) => {
entries.value.set(r.id!, r) entries.value.set(r.id!, r)
updateEntriesStructure()
if (undo) { if (undo) {
registerChange("CREATE", [r]) registerChange("CREATE", [r])
} }
@@ -299,6 +286,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
}) })
} }
/** /**
* delete shopping list entry object from DB and store * delete shopping list entry object from DB and store
* @param object entry object to delete * @param object entry object to delete
@@ -308,6 +296,19 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
const api = new ApiApi() const api = new ApiApi()
return api.apiShoppingListEntryDestroy({id: object.id!}).then((r) => { return api.apiShoppingListEntryDestroy({id: object.id!}).then((r) => {
entries.value.delete(object.id!) 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) { if (undo) {
registerChange("DESTROY", [object]) registerChange("DESTROY", [object])
} }
@@ -331,7 +332,32 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
return recipes 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 * 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 * 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 * @param entry
*/ */
function updateEntryInStructure(structure: IShoppingList, entry: ShoppingListEntry) { function updateEntryInStructure(structure: IShoppingList, entry: ShoppingListEntry) {
let groupingKey = UNDEFINED_CATEGORY
let group = useUserPreferenceStore().deviceSettings.shopping_selected_grouping let group = useUserPreferenceStore().deviceSettings.shopping_selected_grouping
let groupingKey = getEntryCategoryKey(entry)
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)
}
}
}
if (!structure.categories.has(groupingKey) && !(group == ShoppingGroupingOptions.CATEGORY && useUserPreferenceStore().deviceSettings.shopping_show_selected_supermarket_only)) { 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) 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) { if (undo) {
registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries) registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
} }
let entryIdList: number[] = [] let entryIdList: number[] = []
entries.forEach(entry => { entries.forEach(entry => {
entry.checked = checked entry.checked = checked
entryIdList.push(entry.id!) entryIdList.push(entry.id!)
}) })
itemCheckSyncQueue.value.push({ itemCheckSyncQueue.value.push({
ids: entryIdList, ids: entryIdList,
checked: checked, checked: checked,
status: 'waiting', status: 'waiting',
} as IShoppingSyncQueueEntry) } as IShoppingSyncQueueEntry)
runSyncQueue(5)
runSyncQueue(100)
} }
/** /**
@@ -409,15 +419,17 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let api = new ApiApi() let api = new ApiApi()
let promises: Promise<void>[] = [] let promises: Promise<void>[] = []
let updatedEntries = new Map<number, ShoppingListEntry>()
itemCheckSyncQueue.value.forEach((entry, index) => { 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) => { let p = api.apiShoppingListEntryBulkCreate({shoppingListEntryBulk: entry}, {}).then((r) => {
entry.ids.forEach(id => { entry.ids.forEach(id => {
let e = entries.value.get(id) let e = entries.value.get(id)
e.updatedAt = r.timestamp if (e) {
e.checked = r.checked e.updatedAt = r.timestamp
entries.value.set(id, e) updatedEntries.set(id, e)
}
}) })
itemCheckSyncQueue.value.splice(index, 1) itemCheckSyncQueue.value.splice(index, 1)
}).catch((err) => { }).catch((err) => {
@@ -432,6 +444,10 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
}) })
Promise.allSettled(promises).finally(() => { 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) { if (itemCheckSyncQueue.value.length > 0) {
runSyncQueue(500) runSyncQueue(500)
} }
@@ -579,7 +595,8 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
entries, entries,
supermarkets, supermarkets,
supermarketCategories, supermarketCategories,
getEntriesByGroup, updateEntriesStructure,
entriesByGroup,
autoSyncTimeoutId, autoSyncTimeoutId,
autoSyncHasFocus, autoSyncHasFocus,
autoSyncLastTimestamp, autoSyncLastTimestamp,
@@ -589,7 +606,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
hasFailedItems, hasFailedItems,
itemCheckSyncQueue, itemCheckSyncQueue,
undoStack, undoStack,
stats, totalFoods,
refreshFromAPI, refreshFromAPI,
autoSync, autoSync,
createObject, createObject,

View File

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

View File

@@ -7,7 +7,7 @@ import {
MealPlan, MealPlan,
MealType, MealType,
Property, PropertyType, Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry, Space, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingList, ShoppingListEntry, Space,
Step, Step,
Supermarket, Supermarket,
SupermarketCategory, Sync, SyncLog, SupermarketCategory, Sync, SyncLog,
@@ -144,6 +144,7 @@ export type EditorSupportedModels =
| 'Automation' | 'Automation'
| 'Keyword' | 'Keyword'
| 'UserFile' | 'UserFile'
| 'ShoppingList'
| 'ShoppingListEntry' | 'ShoppingListEntry'
| 'User' | 'User'
| 'RecipeBook' | 'RecipeBook'
@@ -182,6 +183,7 @@ export type EditorSupportedTypes =
| Automation | Automation
| Keyword | Keyword
| UserFile | UserFile
| ShoppingList
| ShoppingListEntry | ShoppingListEntry
| User | User
| RecipeBook | RecipeBook
@@ -484,6 +486,28 @@ export const TSupermarketCategory = {
} as Model } as Model
registerModel(TSupermarketCategory) registerModel(TSupermarketCategory)
export const TShoppingList = {
name: 'ShoppingList',
localizationKey: 'ShoppingList',
localizationKeyDescription: 'ShoppingListHelp',
icon: 'fa-solid fa-list-check',
editorComponent: defineAsyncComponent(() => import(`@/components/model_editors/ShoppingListEditor.vue`)),
disableListView: true,
isPaginated: true,
toStringKeys: ['name'],
tableHeaders: [
{title: 'Name', key: 'name'},
{title: 'Color', key: 'color'},
{title: 'Description', key: 'description'},
{title: 'Actions', key: 'action', align: 'end'},
]
} as Model
registerModel(TShoppingList)
export const TShoppingListEntry = { export const TShoppingListEntry = {
name: 'ShoppingListEntry', name: 'ShoppingListEntry',
localizationKey: 'ShoppingListEntry', localizationKey: 'ShoppingListEntry',

View File

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

View File

@@ -1,4 +1,4 @@
import {Supermarket} from "@/openapi"; import {ShoppingList, Supermarket} from "@/openapi";
export type DeviceSettings = { export type DeviceSettings = {
shopping_show_checked_entries: boolean shopping_show_checked_entries: boolean
@@ -6,6 +6,7 @@ export type DeviceSettings = {
shopping_show_selected_supermarket_only: boolean shopping_show_selected_supermarket_only: boolean
shopping_selected_grouping: string shopping_selected_grouping: string
shopping_selected_supermarket: Supermarket | null shopping_selected_supermarket: Supermarket | null
shopping_selected_shopping_list: number[]
shopping_item_info_created_by: boolean shopping_item_info_created_by: boolean
shopping_item_info_mealplan: boolean shopping_item_info_mealplan: boolean
shopping_item_info_recipe: 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) { if (entry.checked && !deviceSettings.shopping_show_checked_entries) {
entryVisible = false 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 return entryVisible
} }
@@ -60,16 +69,15 @@ export function isShoppingListFoodDelayed(slf: IShoppingListFood) {
* @param category * @param category
*/ */
export function isShoppingCategoryVisible(category: IShoppingListCategory) { 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) { return categoryVisible
entryCount += category.stats.countChecked
}
if (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries) {
entryCount += category.stats.countUncheckedDelayed
}
return entryCount > 0
} }
// -------------- SPACE RELATED ---------------------- // -------------- SPACE RELATED ----------------------

View File

@@ -79,4 +79,20 @@ export function stringToBool(param: string): boolean | undefined {
export const routeQueryDateTransformer = { export const routeQueryDateTransformer = {
get: (value: string | null | Date) => ((value == null) ? null : (new Date(value))), get: (value: string | null | Date) => ((value == null) ? null : (new Date(value))),
set: (value: string | null | Date) => ((value == null) ? null : (DateTime.fromJSDate(new Date(value)).toISODate())) set: (value: string | null | Date) => ((value == null) ? null : (DateTime.fromJSDate(new Date(value)).toISODate()))
} }
/**
* routeQueryParam transformer for boolean fields converting string bools to real bools
*/
export const boolOrUndefinedTransformer = {
get: (value: string | null | undefined) => ((value == null) ? undefined : value == 'true'),
set: (value: boolean | null | undefined) => ((value == null) ? undefined : value.toString())
}
/**
* routeQueryParam transformer for number fields converting string numbers to real numbers and allowing undefined for resettable parameters
*/
export const numberOrUndefinedTransformer = {
get: (value: string | null | undefined) => ((value == null) ? undefined : Number(value)),
set: (value: string | null | undefined) => ((value == null) ? undefined : value.toString())
}