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:
has_property_value = True
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
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(

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)))
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')
if less_than or self._timescooked == 0:
if less_than:
default = 1000
else:
default = 0
@@ -338,11 +338,11 @@ class RecipeSearch():
)
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)
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)
elif self._timescooked_gte:
elif self._timescooked_gte is not None:
self._queryset = self._queryset.filter(favorite__gte=int(self._timescooked_gte))
def keyword_filters(self, **kwargs):

View File

@@ -75,7 +75,8 @@ class RecipeShoppingEditor():
@staticmethod
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__in=list(user.get_shopping_share()))
).prefetch_related('entries').first()
@@ -136,7 +137,8 @@ class RecipeShoppingEditor():
self.servings = servings
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._add_ingredients(ingredients=ingredients)
return True
@@ -175,8 +177,9 @@ class RecipeShoppingEditor():
existing = self._shopping_list_recipe.entries.filter(ingredient__in=ingredients).values_list('ingredient__pk', flat=True)
add_ingredients = ingredients.exclude(id__in=existing)
entries = []
for i in [x for x in add_ingredients if x.food]:
ShoppingListEntry.objects.create(
entry = ShoppingListEntry(
list_recipe=self._shopping_list_recipe,
food=i.food,
unit=i.unit,
@@ -185,6 +188,12 @@ class RecipeShoppingEditor():
created_by=self.created_by,
space=self.space,
)
entries.append(entry)
ShoppingListEntry.objects.bulk_create(entries)
for e in entries:
if e.food.shopping_lists.count() > 0:
e.shopping_lists.set(e.food.shopping_lists.all())
# deletes shopping list entries not in ingredients list
def _delete_ingredients(self, ingredients=None):

View File

@@ -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.save()
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(
waiting_time=parse_time(r['perform_time']),
working_time=parse_time(r['prep_time']),
description=r['description'][:512],
name=r['name'],
source_url=r['org_url'],
servings=r['recipe_servings'] if r['recipe_servings'] and r['recipe_servings'] != 0 else 1,
servings_text=r['recipe_yield'].strip() if r['recipe_yield'] else "",
servings=servings,
servings_text=r['recipe_yield'].strip()[:32] if r['recipe_yield'] else "",
internal=True,
created_at=r['created_at'],
space=self.request.space,
@@ -131,7 +137,7 @@ class Mealie1(Integration):
step_id_dict = {}
for s in mealie_database['recipe_instructions']:
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'],
name=s['title'],
space=self.request.space)
@@ -153,7 +159,7 @@ class Mealie1(Integration):
for n in mealie_database['notes']:
if n['recipe_id'] in recipes_dict:
step = Step.objects.create(instruction=n['text'],
name=n['title'],
name=n['title'][:128] if n['title'] else "",
order=100,
space=self.request.space)
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,
)
ingredients_relation.append(Step.ingredients.through(step_id=get_step_id(i, first_step_of_recipe_dict, step_id_dict,recipe_ingredient_ref_link_dict), ingredient_id=ingredient.pk))
elif i['note'].strip():
elif i['note'] and i['note'].strip():
amount, unit, food, note = ingredient_parser.parse(i['note'].strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
@@ -243,7 +249,7 @@ class Mealie1(Integration):
for r in mealie_database['recipe_nutrition']:
if r['recipe_id'] in recipes_dict:
for key in property_types_dict:
if r[key]:
if key in r and r[key]:
properties_relation.append(
Property(property_type_id=property_types_dict[key].pk,
property_amount=Decimal(str(r[key])) / (

View File

@@ -63,7 +63,15 @@ class MealMaster(Integration):
current_recipe = ''
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 current_recipe != '':
recipe_list.append(current_recipe)

View File

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

View File

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

View File

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

View File

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

View File

@@ -88,7 +88,7 @@ from cookbook.models import (Automation, BookmarkletImport, ConnectorConfig, Coo
RecipeBookEntry, ShareLink, ShoppingListEntry,
ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory,
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.local import Local
@@ -114,7 +114,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer,
AiImportSerializer, ImportOpenDataSerializer, ImportOpenDataMetaDataSerializer, ImportOpenDataResponseSerializer, ExportRequestSerializer,
RecipeImportSerializer, ConnectorConfigSerializer, SearchPreferenceSerializer, SearchFieldsSerializer, RecipeBatchUpdateSerializer,
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer, GenericModelReferenceSerializer
AiProviderSerializer, AiLogSerializer, FoodBatchUpdateSerializer, GenericModelReferenceSerializer, ShoppingListSerializer
)
from cookbook.version_info import TANDOOR_VERSION
from cookbook.views.import_export import get_integration
@@ -307,7 +307,8 @@ class FuzzyFilterMixin(viewsets.ModelViewSet, ExtendedRecipeMixin):
filter = Q(name__icontains=query)
if self.request.user.is_authenticated:
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)
self.queryset = (
@@ -1991,25 +1992,39 @@ class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
if serializer.is_valid():
entries = []
for e in serializer.validated_data['entries']:
entries.append(
ShoppingListEntry(
list_recipe_id=obj.pk,
amount=e['amount'],
unit_id=e['unit_id'],
food_id=e['food_id'],
ingredient_id=e['ingredient_id'],
created_by_id=request.user.id,
space_id=request.space.id,
)
entry = ShoppingListEntry(
list_recipe_id=obj.pk,
amount=e['amount'],
unit_id=e['unit_id'],
food_id=e['food_id'],
ingredient_id=e['ingredient_id'],
created_by_id=request.user.id,
space_id=request.space.id,
)
entries.append(entry)
ShoppingListEntry.objects.bulk_create(entries)
for e in entries:
if e.food.shopping_lists.count() > 0:
e.shopping_lists.set(e.food.shopping_lists.all())
ConnectorManager.add_work(ActionType.CREATED, *entries)
return Response(serializer.validated_data)
else:
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=[
OpenApiParameter(name='updated_after',
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):
self.queryset = self.queryset.filter(space=self.request.space)
# select_related("list_recipe")
self.queryset = self.queryset.filter(
Q(created_by=self.request.user)
| Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by', 'food',
'food__properties',
'food__properties__property_type',
'food__inherit_fields',
'food__supermarket_category',
'food__onhand_users',
'food__substitute',
'food__child_inherit_fields',
'unit', 'list_recipe',
| Q(created_by__in=list(self.request.user.get_shopping_share()))).prefetch_related('created_by',
'food',
'food__shopping_lists',
'shopping_lists',
'unit',
'list_recipe',
'list_recipe__recipe__keywords',
'list_recipe__recipe__created_by',
'list_recipe__mealplan',
'list_recipe__mealplan__shared',
'list_recipe__mealplan__shared__userspace_set',
'list_recipe__mealplan__shoppinglistrecipe_set',
'list_recipe__mealplan__recipe',
'list_recipe__mealplan__recipe__keywords',
).distinct().all()
updated_after = self.request.query_params.get('updated_after', None)
@@ -3056,11 +3075,20 @@ def meal_plans_to_ical(queryset, filename):
for p in queryset:
event = Event()
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:
event.add('dtend', p.to_date)
else:
event.add('dtend', p.from_date)
end_date_time = p.to_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['description'] = p.note
cal.add_component(event)

View File

@@ -43,7 +43,10 @@ def index(request, path=None, resource=None):
return HttpResponseRedirect(reverse_lazy('view_setup'))
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'):
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))
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
django-annoying==0.10.6
django-cleanup==9.0.0

View File

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

View File

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

View File

@@ -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-list-item>-->
<!-- <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="Space" @click="window = 'space'" prepend-icon="fa-solid fa-database"></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="$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('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('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('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('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>
@@ -31,6 +31,8 @@
<v-main>
<v-container>
<v-select v-model="window" :items="mobileMenuItems" class="d-block d-lg-none"> </v-select>
<v-window v-model="window">
<v-window-item value="start">
<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>
<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>
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.
@@ -60,10 +63,12 @@
</v-window-item>
<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>
<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
using the settings.
</p>
@@ -77,19 +82,24 @@
</v-window-item>
<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.
</p>
<p class="mt-3">Besides manually creating them you can also import them from various different places.
</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.
You can also create a share link for the recipe to share it with everyone that has access to the link.
</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-window-item>
@@ -119,7 +129,8 @@
</p>
<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 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
@@ -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'}}">
{{ $t('Unit') }}
</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') }}
</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).
</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') }}
</v-btn>
<h3>Editor</h3>
@@ -294,7 +307,8 @@
</p>
<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.
</p>
@@ -333,7 +347,8 @@
<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
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.
</p>
@@ -368,10 +383,30 @@
import {ref} from "vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
import {useI18n} from "vue-i18n";
const {t} = useI18n()
const drawer = defineModel()
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>

View File

@@ -146,7 +146,11 @@ onMounted(() => {
function clickMealPlan(plan: MealPlan) {
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-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-card-text>
<v-list>
<v-list-item border v-for="fv in dialogProperty.foodValues" :key="`${dialogProperty.id}_${fv.id}`">
<template #prepend>
<v-progress-circular size="55" width="5" :model-value="(fv.value/dialogProperty.propertyAmountTotal)*100"
:color="colorScale((fv.value/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null && dialogProperty.propertyAmountTotal > 0">
{{ Math.round((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* props.ingredientFactor/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null && dialogProperty.propertyAmountTotal > 0">
{{ Math.round((fv.value* props.ingredientFactor / dialogProperty.propertyAmountTotal) * 100) }}%
</v-progress-circular>
<v-progress-circular size="55" width="5" v-if="fv.value == null">?</v-progress-circular>
</template>
@@ -59,7 +59,7 @@
<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>
</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">
{{ $t('NoUnit') }}
</v-chip>
@@ -101,7 +101,10 @@ type PropertyWrapper = {
}
const props = defineProps({
servings: {type: Number, required: true,},
ingredientFactor: {
type: Number,
required: true,
},
})
const recipe = defineModel<Recipe>({required: true})
@@ -143,7 +146,7 @@ const propertyList = computed(() => {
description: rp.propertyType.description,
foodValues: [],
propertyAmountPerServing: rp.propertyAmount,
propertyAmountTotal: rp.propertyAmount * recipe.value.servings * (props.servings / recipe.value.servings),
propertyAmountTotal: rp.propertyAmount * recipe.value.servings * props.ingredientFactor,
missingValue: false,
unit: rp.propertyType.unit,
type: rp.propertyType,
@@ -161,7 +164,7 @@ const propertyList = computed(() => {
icon: fp.icon,
foodValues: fp.food_values,
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,
unit: fp.unit,
type: fp,

View File

@@ -69,7 +69,7 @@
<script setup lang="ts">
import {onMounted, PropType, ref} from "vue";
import {onMounted, PropType, ref, watch} from "vue";
import {ApiApi, CookLog, Recipe} from "@/openapi";
import {DateTime} from "luxon";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
@@ -82,6 +82,10 @@ const props = defineProps({
type: Object as PropType<Recipe>,
required: true
},
servings: {
type: Number,
required: true
}
})
const newCookLog = ref({} as CookLog);
@@ -121,7 +125,7 @@ function recLoadCookLog(recipeId: number, page: number = 1) {
*/
function resetForm() {
newCookLog.value = {} as CookLog
newCookLog.value.servings = props.recipe.servings
newCookLog.value.servings = props.servings
newCookLog.value.createdAt = new Date()
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>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<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>
@@ -36,7 +36,7 @@
</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
class="align-center justify-center"
location="top center" origin="overlap"
@@ -97,7 +97,7 @@
</template>
<script setup lang="ts">
import {PropType} from 'vue'
import {computed, PropType} from 'vue'
import KeywordsComponent from "@/components/display/KeywordsBar.vue";
import {Recipe, RecipeOverview} from "@/openapi";
@@ -113,20 +113,29 @@ const props = defineProps({
show_description: {type: Boolean, required: false},
height: {type: String, required: false, default: '15vh'},
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 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
*/
function openRecipe() {
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);
} 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 class="d-block d-lg-none">
<template class="d-block d-lg-none d-print-none">
<!-- mobile layout -->
<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">
{{ recipe.name }}
</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>
<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>
@@ -61,7 +61,7 @@
</v-card>
</template>
<!-- Desktop horizontal layout -->
<template class="d-none d-lg-block">
<template class="d-none d-lg-block d-print-block">
<v-row dense>
<v-col cols="8">
<recipe-image
@@ -75,7 +75,7 @@
<v-card-text class="flex-grow-1">
<div class="d-flex">
<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>
</div>
<p>
@@ -118,7 +118,7 @@
</v-row>
</template>
<template v-if="recipe.filePath">
<template v-if="recipe.filePath && !useUserPreferenceStore().isPrintMode">
<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"
@@ -144,7 +144,7 @@
<step-view v-model="recipe.steps[index]" :step-number="index+1" :ingredientFactor="ingredientFactor"></step-view>
</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-text>
@@ -190,7 +190,7 @@
</v-card-text>
</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>
@@ -220,8 +220,11 @@ const {doAiImport, fileApiLoading} = useFileApi()
const loading = ref(false)
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 selectedAiProvider = ref<undefined | AiProvider>(useUserPreferenceStore().activeSpace.aiDefaultProvider)
@@ -236,11 +239,13 @@ const ingredientFactor = computed(() => {
/**
* change servings when recipe servings are changed
*/
watch(() => recipe.value.servings, () => {
if (recipe.value.servings) {
servings.value = recipe.value.servings
}
})
if (props.servings === undefined) {
watch(() => recipe.value.servings, () => {
if (recipe.value.servings) {
servings.value = recipe.value.servings
}
})
}
onMounted(() => {
//keep screen on while viewing a recipe

View File

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

View File

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

View File

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

View File

@@ -22,10 +22,10 @@
<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-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>
</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"
v-if="step.instructionsMarkdown != undefined"></instructions>
<!-- 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 Timer from "@/components/display/Timer.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.ts";
const step = defineModel<Step>({required: true})

View File

@@ -26,7 +26,7 @@
<v-progress-circular v-if="duplicateLoading" indeterminate size="small"></v-progress-circular>
</template>
</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') }}
</v-list-item>
</v-list>
@@ -47,22 +47,21 @@ import AddToShoppingDialog from "@/components/dialogs/AddToShoppingDialog.vue";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore.ts";
import {useRouter} from "vue-router";
import {useFileApi} from "@/composables/useFileApi.ts";
import {useI18n} from "vue-i18n";
const router = useRouter()
const {t} = useI18n()
const {updateRecipeImage} = useFileApi()
const props = defineProps({
recipe: {type: Object as PropType<Recipe | RecipeOverview>, required: true},
servings: {type: Number, default: undefined},
size: {type: String, default: 'medium'},
})
const mealPlanDialog = 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
*/
@@ -70,7 +69,27 @@ function duplicateRecipe() {
let api = new ApiApi()
duplicateLoading.value = true
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) {
updateRecipeImage(newRecipe.id!, null, originalRecipe.image).then(r => {

View File

@@ -283,8 +283,9 @@ function parseAndInsertIngredients() {
}
})
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 => {
console.log(i)
step.value.ingredients.push({
originalText: i.value.originalText,
amount: i.value.amount,

View File

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

View File

@@ -29,7 +29,7 @@
@update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect>
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>-->
<!--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()">
{{$t('Add')}}
<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('shopping_add_onhand')" :hint="$t('shopping_add_onhand_desc')" persistent-hint v-model="useUserPreferenceStore().userSettings.shoppingAddOnhand"></v-checkbox>
<v-checkbox :label="$t('filter_to_supermarket')" :hint="$t('filter_to_supermarket_desc')" persistent-hint v-model="useUserPreferenceStore().userSettings.filterToSupermarket"></v-checkbox>
<v-checkbox :label="$t('UpdateFoodLists')" :hint="$t('UpdateFoodListsHelp')" persistent-hint v-model="useUserPreferenceStore().userSettings.shoppingUpdateFoodLists"></v-checkbox>
<v-number-input
class="mt-2"

View File

@@ -16,7 +16,7 @@ export function useNavigation() {
{component: VListItem, prependIcon: '$recipes', title: 'Home', to: {name: 'StartPage', 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: '$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: '$books', title: t('Books'), to: {name: 'BooksPage', params: {}}},
{component: VListItem, prependIcon: 'fa-solid fa-folder-tree', title: t('Database'), to: {name: 'DatabasePage', params: {}}},

View File

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

View File

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

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Alineació",
"All": "",
"Amount": "Quantitat",
"App": "Aplicació",
"Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Avaluació",
"Recently_Viewed": "Vistos recentment",
"Recipe": "Recepta",
"RecipeStructure": "",
"Recipe_Book": "Llibre de receptes",
"Recipe_Image": "Imatge de la recepta",
"Recipes": "Receptes",
@@ -394,6 +396,7 @@
"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.",
"Split_All_Steps": "Dividir totes les files en passos separats.",
"Start": "",
"StartDate": "Data d'inici",
"Starting_Day": "Dia d'inici de la setmana",
"StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Zarovnání",
"All": "",
"Amount": "Množství",
"App": "Aplikace",
"Apply": "",
@@ -334,6 +335,7 @@
"Ratings": "Hodnocení",
"Recently_Viewed": "Naposledy prohlížené",
"Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Kuchařka",
"Recipe_Image": "Obrázek k receptu",
"Recipes": "Recepty",
@@ -389,6 +391,7 @@
"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.",
"Split_All_Steps": "Rozdělit každý řádek do samostatného kroku.",
"Start": "",
"StartDate": "Počáteční datum",
"Starting_Day": "První den v týdnu",
"StartsWith": "",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Justering",
"All": "",
"Amount": "Mængde",
"App": "App",
"Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Bedømmelser",
"Recently_Viewed": "Vist for nylig",
"Recipe": "Opskrift",
"RecipeStructure": "",
"Recipe_Book": "Opskriftsbog",
"Recipe_Image": "Opskriftsbillede",
"Recipes": "Opskrifter",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "",
"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.",
"Start": "",
"StartDate": "Startdato",
"Starting_Day": "Første dag på ugen",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -37,6 +37,7 @@
"AiProvider": "AI Provider",
"AiProviderHelp": "You can configure multiple AI providers according to your preferences. They can even be configured to work across multiple spaces.",
"Alignment": "Alignment",
"All": "All",
"AllRecipes": "All Recipes",
"Amount": "Amount",
"App": "App",
@@ -149,6 +150,7 @@
"Decimals": "Decimals",
"Default": "Default",
"DefaultPage": "Default Page",
"DefaultShoppingListHelp": "Default List when this Food is added to the Shoppinglist.",
"Default_Unit": "Default Unit",
"DelayFor": "Delay for {hours} hours",
"DelayUntil": "Delay Until",
@@ -373,6 +375,7 @@
"NoUnit": "No Unit",
"No_ID": "ID not found, cannot delete.",
"No_Results": "No Results",
"None": "None",
"NotFound": "Not found",
"NotFoundHelp": "The page or object you are looking for could not be found.",
"NotInShopping": "{food} is not in your shopping list.",
@@ -452,6 +455,7 @@
"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. ",
"RecipeStepsHelp": "Ingredients, Instructions and more can be edited in the tab Steps.",
"RecipeStructure": "Recipe Structure",
"Recipe_Book": "Recipe Book",
"Recipe_Image": "Recipe Image",
"Recipes": "Recipes",
@@ -500,14 +504,17 @@
"Share": "Share",
"ShopLater": "Shop later",
"ShopNow": "Shop now",
"Shopping": "Shopping",
"ShoppingBackgroundSyncWarning": "Bad network, waiting to sync ...",
"ShoppingList": "Shoppinglist",
"ShoppingListEntry": "Shoppinglist Entry",
"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",
"Shopping_Categories": "Shopping Categories",
"Shopping_Category": "Shopping Category",
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
"Shopping_input_placeholder": "e.g. Potato/100 Potatoes/100 g Potatoes",
"Shopping_input_placeholder": "e.g. 100 g Potatoes",
"Shopping_list": "Shopping List",
"ShowDelayed": "Show delayed items",
"ShowIngredients": "Show Ingredients",
@@ -539,6 +546,7 @@
"Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.",
"Split": "Split",
"Split_All_Steps": "Split all rows into separate steps.",
"Start": "Start",
"StartDate": "Start Date",
"Starting_Day": "Starting day of the week",
"StartsWith": "Starts with",
@@ -608,6 +616,8 @@
"Unrated": "Unrated",
"Up": "Up",
"Update": "Update",
"UpdateFoodLists": "Update Food Shoppinglists",
"UpdateFoodListsHelp": "Update the default shopping lists in the food when changing shopping lists during shopping.",
"Update_Existing_Data": "Update Existing Data",
"Updated": "Updated",
"UpgradeNow": "Upgrade now",

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Poravnanje",
"All": "",
"Amount": "Količina",
"App": "Aplikacija",
"Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Ocjene",
"Recently_Viewed": "Nedavno pogledano",
"Recipe": "Recept",
"RecipeStructure": "",
"Recipe_Book": "Knjiga recepata",
"Recipe_Image": "Slika recepta",
"Recipes": "Recepti",
@@ -394,6 +396,7 @@
"SpacePrivateObjectsHelp": "",
"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.",
"Start": "",
"StartDate": "Početni datum",
"Starting_Day": "Početni dan u tjednu",
"StartsWith": "",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
"AIImportSubtitle": "Gebruik Al om afbeeldingen van recepten te importeren.",
"AISettingsHostedHelp": "Je kunt AI-functies inschakelen of beschikbare credits aanpassen door je abonnement te beheren.",
"API": "API",
"APIKey": "API-sleutel",
"API_Browser": "API-browser",
"API_Documentation": "API-documentatie",
"AccessTokenHelp": "Toegangssleutels voor de REST API.",
@@ -55,7 +56,7 @@
"BaseUnit": "Basiseenheid",
"BaseUnitHelp": "Standaardeenheid om automatische eenheden om te rekenen",
"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. ",
"BatchEdit": "Batchbewerking",
"BatchEditUpdatingItemsCount": "{count} {type} bewerken",
@@ -329,6 +330,7 @@
"Miscellaneous": "Diversen",
"MissingConversion": "Ontbrekende conversie",
"MissingProperties": "Ontbrekende eigenschappen",
"Model": "Model",
"ModelSelectResultsHelp": "Zoek naar meer resultaten",
"Monday": "Maandag",
"Month": "Maand",
@@ -448,6 +450,7 @@
"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. ",
"RecipeStepsHelp": "Ingrediënten, instructies en meer kun je bewerken in het tabblad stappen.",
"RecipeStructure": "Receptstructuur",
"Recipe_Book": "Kookboek",
"Recipe_Image": "Receptafbeelding",
"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.",
"Split": "Splitsen",
"Split_All_Steps": "Splits alle rijen in aparte stappen.",
"Start": "Start",
"StartDate": "Startdatum",
"Starting_Day": "Eerste dag van de week",
"StartsWith": "Begint met",
@@ -550,10 +554,12 @@
"Storage": "Externe opslag",
"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. ",
"Structured": "Gestructureerd",
"SubstituteOnHand": "Je hebt een alternatief op voorraad.",
"Substitutes": "Alternatieven",
"Success": "Succes",
"SuccessClipboard": "Boodschappenlijst is gekopieerd naar klembord",
"Summary": "Samenvatting",
"Sunday": "Zondag",
"Supermarket": "Supermarkt",
"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_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?",
"Summary": "Samenvatting",
"APIKey": "API-sleutel",
"Model": "Model",
"Structured": "Gestructureerd"
"AboutTandoor": "Tandoor is een open source platform om recepten, maaltijdplannen, boodschappenlijstjes en meer te beheren.",
"ImportIntoTandoorHelp": "Om dit recept in je eigen Tandoor-collectie te importeren, volg je de volgende stappen.",
"SelfHosted": "Zelfgehost",
"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": "",
"AiProviderHelp": "",
"Alignment": "Wyrównanie",
"All": "",
"AllRecipes": "Wszystkie przepisy",
"Amount": "Ilość",
"App": "Aplikacja",
@@ -363,6 +364,7 @@
"Ratings": "Oceny",
"Recently_Viewed": "Ostatnio oglądane",
"Recipe": "Przepis",
"RecipeStructure": "",
"Recipe_Book": "Książka z przepisami",
"Recipe_Image": "Obrazek dla przepisu",
"Recipes": "Przepisy",
@@ -420,6 +422,7 @@
"SpacePrivateObjectsHelp": "",
"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.",
"Start": "",
"StartDate": "Data początkowa",
"Starting_Day": "Dzień rozpoczęcia tygodnia",
"StartsWith": "",

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"AiModelHelp": "",
"AiProvider": "",
"AiProviderHelp": "",
"All": "",
"Amount": "Cantitate",
"App": "Aplicație",
"Apply": "",
@@ -298,6 +299,7 @@
"Ratings": "Evaluări",
"Recently_Viewed": "Vizualizate recent",
"Recipe": "Rețetă",
"RecipeStructure": "",
"Recipe_Book": "Carte de rețete",
"Recipe_Image": "Imagine a rețetei",
"Recipes": "Rețete",
@@ -349,6 +351,7 @@
"SpaceName": "",
"SpacePrivateObjectsHelp": "",
"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",
"StartsWith": "",
"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.",
"AISettingsHostedHelp": "Funkcije umetne inteligence lahko omogočite ali spremenite razpoložljive kredite z upravljanjem naročnine.",
"API": "API",
"APIKey": "API ključ",
"API_Browser": "API brskalnik",
"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.",
"Access_Token": "Dostopni žeton",
"Account": "Račun",
@@ -115,6 +117,7 @@
"Create": "Ustvari",
"Create Food": "Ustvari živilo",
"Create Recipe": "Ustvari recept",
"CreateAccount": "Ustvari račun",
"CreateFirstRecipe": "Ustvarite svoj prvi recept z urejevalnikom receptov.",
"CreateInvitation": "Ustvari povabilo",
"Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok",
@@ -208,6 +211,7 @@
"Fats": "Maščobe",
"File": "Datoteka",
"Files": "Datoteke",
"Finish": "Končaj",
"FinishedAt": "Končano ob",
"First": "Prvi",
"First_name": "Ime",
@@ -256,6 +260,7 @@
"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.",
"ImportIntoTandoor": "Uvozi v Tandoor",
"ImportIntoTandoorHelp": "Če želite uvoziti ta recept v svojo zbirko Tandoor, sledite naslednjim korakom.",
"ImportMealPlans": "Uvozi načrte prehrane",
"ImportShoppingList": "Uvozi nakupovalne sezname",
"Import_Error": "Med uvozom je prišlo do napake. Za ogled razširite podrobnosti na dnu strani.",
@@ -328,6 +333,7 @@
"Miscellaneous": "Razno",
"MissingConversion": "Manjkajoča konverzija",
"MissingProperties": "Manjkajoče lastnosti",
"Model": "Model",
"ModelSelectResultsHelp": "Išči več rezultatov",
"Monday": "Ponedeljek",
"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. ",
"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.",
"RecipeStructure": "Struktura recepta",
"Recipe_Book": "Knjiga receptov",
"Recipe_Image": "Slika recepta",
"Recipes": "Recepti",
@@ -486,6 +493,7 @@
"Select_File": "Izberi datoteko",
"Selected": "Izbrano",
"SelectedCategories": "Izbrane kategorije",
"SelfHosted": "Samostojno gostovanje",
"Serving": "Serviranje",
"Servings": "Porcije",
"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.",
"Split": "Razdelitev",
"Split_All_Steps": "Vse vrstice razdelite na ločene korake.",
"Start": "Začni",
"StartDate": "Začetni datum",
"Starting_Day": "Začetni dan v tednu",
"StartsWith": "Začne se s/z",
@@ -549,10 +558,12 @@
"Storage": "Zunanji pomnilnik",
"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. ",
"Structured": "Strukturirano",
"SubstituteOnHand": "Pri roki imate nadomestek.",
"Substitutes": "Nadomestki",
"Success": "Uspešno",
"SuccessClipboard": "Nakupovalni listek je kopiran v odložišče",
"Summary": "Povzetek",
"Sunday": "Nedelja",
"Supermarket": "Trgovina",
"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_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?",
"APIKey": "API ključ",
"Model": "Model",
"Structured": "Strukturirano",
"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"
"Shopping": "Nakupovanje",
"ShoppingList": "Nakupovalni seznam",
"ShoppingListHelp": "Omogoča dodajanje vnosov na različne sezname. Uporablja se lahko za različne supermarkete, posebne ponudbe ali dogodke. "
}

View File

@@ -36,6 +36,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Orientering",
"All": "",
"AllRecipes": "Alla recept",
"Amount": "Mängd",
"App": "App",
@@ -150,11 +151,13 @@
"DelayFor": "Fördröjning på {hours} timmar",
"DelayUntil": "Fördröjning till",
"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?",
"DeleteSomething": "",
"Delete_All": "Radera alla",
"Delete_Food": "Ta bort livsmedel",
"Delete_Keyword": "Ta bort nyckelord",
"Deleted": "Borttagen",
"Description": "Beskrivning",
"Description_Replace": "Ersätt beskrivning",
"Disable": "Inaktivera",

View File

@@ -26,6 +26,7 @@
"AiProvider": "",
"AiProviderHelp": "",
"Alignment": "Hizalama",
"All": "",
"Amount": "Miktar",
"App": "Uygulama",
"Apply": "",
@@ -337,6 +338,7 @@
"Ratings": "Derecelendirmeler",
"Recently_Viewed": "Son Görüntülenen",
"Recipe": "Tarif",
"RecipeStructure": "",
"Recipe_Book": "Yemek Tarifi Kitabı",
"Recipe_Image": "Tarif Resmi",
"Recipes": "Tarifler",
@@ -394,6 +396,7 @@
"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.",
"Split_All_Steps": "Tüm satırları ayrı adımlara bölün.",
"Start": "",
"StartDate": "Başlangıç Tarihi",
"Starting_Day": "Haftanın başlangıç günü",
"StartsWith": "",

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -85,6 +85,7 @@ import type {
PaginatedRecipeImportList,
PaginatedRecipeOverviewList,
PaginatedShoppingListEntryList,
PaginatedShoppingListList,
PaginatedShoppingListRecipeList,
PaginatedSpaceList,
PaginatedStepList,
@@ -131,6 +132,7 @@ import type {
PatchedRecipeBookEntry,
PatchedRecipeImport,
PatchedSearchPreference,
PatchedShoppingList,
PatchedShoppingListEntry,
PatchedShoppingListRecipe,
PatchedSpace,
@@ -163,6 +165,7 @@ import type {
SearchPreference,
ServerSettings,
ShareLink,
ShoppingList,
ShoppingListEntry,
ShoppingListEntryBulk,
ShoppingListEntryBulkCreate,
@@ -324,6 +327,8 @@ import {
PaginatedRecipeOverviewListToJSON,
PaginatedShoppingListEntryListFromJSON,
PaginatedShoppingListEntryListToJSON,
PaginatedShoppingListListFromJSON,
PaginatedShoppingListListToJSON,
PaginatedShoppingListRecipeListFromJSON,
PaginatedShoppingListRecipeListToJSON,
PaginatedSpaceListFromJSON,
@@ -416,6 +421,8 @@ import {
PatchedRecipeImportToJSON,
PatchedSearchPreferenceFromJSON,
PatchedSearchPreferenceToJSON,
PatchedShoppingListFromJSON,
PatchedShoppingListToJSON,
PatchedShoppingListEntryFromJSON,
PatchedShoppingListEntryToJSON,
PatchedShoppingListRecipeFromJSON,
@@ -480,6 +487,8 @@ import {
ServerSettingsToJSON,
ShareLinkFromJSON,
ShareLinkToJSON,
ShoppingListFromJSON,
ShoppingListToJSON,
ShoppingListEntryFromJSON,
ShoppingListEntryToJSON,
ShoppingListEntryBulkFromJSON,
@@ -1955,6 +1964,21 @@ export interface ApiShareLinkRetrieveRequest {
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 {
shoppingListEntryBulk: Omit<ShoppingListEntryBulk, 'timestamp'>;
}
@@ -1988,6 +2012,30 @@ export interface ApiShoppingListEntryUpdateRequest {
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 {
id: number;
shoppingListEntryBulkCreate: ShoppingListEntryBulkCreate;
@@ -2021,6 +2069,15 @@ export interface ApiShoppingListRecipeUpdateRequest {
shoppingListRecipe: Omit<ShoppingListRecipe, 'recipeData'|'mealPlanData'|'createdBy'>;
}
export interface ApiShoppingListRetrieveRequest {
id: number;
}
export interface ApiShoppingListUpdateRequest {
id: number;
shoppingList?: ShoppingList;
}
export interface ApiSpaceCreateRequest {
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();
}
/**
* 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
*/
@@ -14837,6 +15012,182 @@ export class ApiApi extends runtime.BaseAPI {
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/
*/
@@ -15126,6 +15477,83 @@ export class ApiApi extends runtime.BaseAPI {
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/
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,7 @@
</v-row>
<v-row dense>
<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="MealType"></database-model-col>
</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-else color="info" @click="useUserPreferenceStore().switchSpace(item)">{{ $t('Select') }}</v-chip>
</template>
<template v-slot:item.color="{ item }">
<v-chip label :color="item.color">{{ item.color }}</v-chip>
</template>
<template v-slot:item.action="{ item }">
<v-btn class="float-right" icon="$menu" variant="plain">
<v-icon icon="$menu"></v-icon>

View File

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

View File

@@ -2,7 +2,8 @@
<v-container :class="{'ps-0 pe-0 pt-0': mobile}">
<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">
<import-tandoor-dialog></import-tandoor-dialog>
@@ -35,6 +36,13 @@ const isShared = computed(() => {
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)
watch(() => props.id, () => {

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import {
MealPlan,
MealType,
Property, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingListEntry, Space,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchFields, ShoppingList, ShoppingListEntry, Space,
Step,
Supermarket,
SupermarketCategory, Sync, SyncLog,
@@ -144,6 +144,7 @@ export type EditorSupportedModels =
| 'Automation'
| 'Keyword'
| 'UserFile'
| 'ShoppingList'
| 'ShoppingListEntry'
| 'User'
| 'RecipeBook'
@@ -182,6 +183,7 @@ export type EditorSupportedTypes =
| Automation
| Keyword
| UserFile
| ShoppingList
| ShoppingListEntry
| User
| RecipeBook
@@ -484,6 +486,28 @@ export const TSupermarketCategory = {
} as Model
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 = {
name: 'ShoppingListEntry',
localizationKey: 'ShoppingListEntry',

View File

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

View File

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

View File

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

View File

@@ -79,4 +79,20 @@ export function stringToBool(param: string): boolean | undefined {
export const routeQueryDateTransformer = {
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()))
}
}
/**
* 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())
}