Merge branch 'develop' into docs/docker-installation
This commit is contained in:
commit
075c88e5e8
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: Continous Integration
|
name: Continuous Integration
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continuous%20Integration/badge.svg?branch=master" ></a>
|
||||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||||
|
@ -7,4 +7,4 @@ Since this software is still considered beta/WIP support is always only given fo
|
|||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please open a normal public issue if you have any security related concerns. If you feel like the issue should not be discussed in
|
Please open a normal public issue if you have any security related concerns. If you feel like the issue should not be discussed in
|
||||||
public just open a generic issue and we will discuss further communitcation there (since GitHub does not allow everyone to create a security advisory :/).
|
public just open a generic issue and we will discuss further communication there (since GitHub does not allow everyone to create a security advisory :/).
|
||||||
|
@ -442,7 +442,7 @@ class SearchPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'search': _(
|
'search': _(
|
||||||
'Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
|
'Select type method of search. Click <a href="/docs/search/">here</a> for full description of choices.'),
|
||||||
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
|
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
|
||||||
'unaccent': _(
|
'unaccent': _(
|
||||||
'Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
|
'Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
|
||||||
@ -461,7 +461,7 @@ class SearchPreferenceForm(forms.ModelForm):
|
|||||||
'lookup': _('Fuzzy Lookups'),
|
'lookup': _('Fuzzy Lookups'),
|
||||||
'unaccent': _('Ignore Accent'),
|
'unaccent': _('Ignore Accent'),
|
||||||
'icontains': _("Partial Match"),
|
'icontains': _("Partial Match"),
|
||||||
'istartswith': _("Starts Wtih"),
|
'istartswith': _("Starts With"),
|
||||||
'trigram': _("Fuzzy Search"),
|
'trigram': _("Fuzzy Search"),
|
||||||
'fulltext': _("Full Text")
|
'fulltext': _("Full Text")
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ def has_group_permission(user, groups):
|
|||||||
"""
|
"""
|
||||||
Tests if a given user is member of a certain group (or any higher group)
|
Tests if a given user is member of a certain group (or any higher group)
|
||||||
Superusers always bypass permission checks.
|
Superusers always bypass permission checks.
|
||||||
Unauthenticated users cant be member of any group thus always return false.
|
Unauthenticated users can't be member of any group thus always return false.
|
||||||
:param user: django auth user object
|
:param user: django auth user object
|
||||||
:param groups: list or tuple of groups the user should be checked for
|
:param groups: list or tuple of groups the user should be checked for
|
||||||
:return: True if user is in allowed groups, false otherwise
|
:return: True if user is in allowed groups, false otherwise
|
||||||
|
@ -126,7 +126,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
|||||||
add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
add_ingredients = set(add_ingredients) - set(existing_list.values_list('ingredient__id', flat=True))
|
||||||
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
|
||||||
|
|
||||||
# if servings have changed, update the ShoppingListRecipe and existing Entrys
|
# if servings have changed, update the ShoppingListRecipe and existing Entries
|
||||||
if servings <= 0:
|
if servings <= 0:
|
||||||
servings = 1
|
servings = 1
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
|||||||
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
|
||||||
sle.save()
|
sle.save()
|
||||||
|
|
||||||
# add any missing Entrys
|
# add any missing Entries
|
||||||
for i in [x for x in add_ingredients if x.food]:
|
for i in [x for x in add_ingredients if x.food]:
|
||||||
|
|
||||||
ShoppingListEntry.objects.create(
|
ShoppingListEntry.objects.create(
|
||||||
|
@ -27,7 +27,7 @@ class RecetteTek(Integration):
|
|||||||
|
|
||||||
def get_recipe_from_file(self, file):
|
def get_recipe_from_file(self, file):
|
||||||
|
|
||||||
# Create initial recipe with just a title and a decription
|
# Create initial recipe with just a title and a description
|
||||||
recipe = Recipe.objects.create(name=file['title'], created_by=self.request.user, internal=True, space=self.request.space, )
|
recipe = Recipe.objects.create(name=file['title'], created_by=self.request.user, internal=True, space=self.request.space, )
|
||||||
|
|
||||||
# set the description as an empty string for later use for the source URL, in case there is no description text.
|
# set the description as an empty string for later use for the source URL, in case there is no description text.
|
||||||
|
@ -15,10 +15,10 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||||
"PO-Revision-Date: 2021-11-12 20:06+0000\n"
|
"PO-Revision-Date: 2022-01-20 22:47+0000\n"
|
||||||
"Last-Translator: A. L. <richard@anska.de>\n"
|
"Last-Translator: Sebastian Weber <tandoor@web3r.de>\n"
|
||||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
|
||||||
"backend/de/>\n"
|
"recipes-backend/de/>\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
@ -122,10 +122,8 @@ msgstr ""
|
|||||||
"sollen."
|
"sollen."
|
||||||
|
|
||||||
#: .\cookbook\forms.py:80
|
#: .\cookbook\forms.py:80
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Try the new shopping list"
|
|
||||||
msgid "Users with whom to share shopping lists."
|
msgid "Users with whom to share shopping lists."
|
||||||
msgstr "Neue Einkaufsliste ausprobieren"
|
msgstr "Benutzer, mit denen Einkaufslisten geteilt werden sollen."
|
||||||
|
|
||||||
#: .\cookbook\forms.py:82
|
#: .\cookbook\forms.py:82
|
||||||
msgid "Show recently viewed recipes on search page."
|
msgid "Show recently viewed recipes on search page."
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
if settings.DATABASES['default']['ENGINE'] not in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||||
self.stdout.write(self.style.WARNING(_('Only Postgress databases use full text search, no index to rebuild')))
|
self.stdout.write(self.style.WARNING(_('Only Postgresql databases use full text search, no index to rebuild')))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
language = DICTIONARY.get(translation.get_language(), 'simple')
|
language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 3.2.11 on 2022-01-20 14:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_trigram(apps, schema_editor):
|
||||||
|
with scopes_disabled():
|
||||||
|
UserPreference = apps.get_model('cookbook', 'UserPreference')
|
||||||
|
|
||||||
|
UserPreference.objects.all().update(shopping_add_onhand=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0165_remove_step_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='shopping_add_onhand',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.RunPython(add_default_trigram),
|
||||||
|
]
|
18
cookbook/migrations/0167_userpreference_left_handed.py
Normal file
18
cookbook/migrations/0167_userpreference_left_handed.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.11 on 2022-01-20 22:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0166_alter_userpreference_shopping_add_onhand'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='left_handed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -337,8 +337,9 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
mealplan_autoadd_shopping = models.BooleanField(default=False)
|
mealplan_autoadd_shopping = models.BooleanField(default=False)
|
||||||
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
||||||
mealplan_autoinclude_related = models.BooleanField(default=True)
|
mealplan_autoinclude_related = models.BooleanField(default=True)
|
||||||
shopping_add_onhand = models.BooleanField(default=True)
|
shopping_add_onhand = models.BooleanField(default=False)
|
||||||
filter_to_supermarket = models.BooleanField(default=False)
|
filter_to_supermarket = models.BooleanField(default=False)
|
||||||
|
left_handed = models.BooleanField(default=False)
|
||||||
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
||||||
shopping_recent_days = models.PositiveIntegerField(default=7)
|
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||||
csv_delim = models.CharField(max_length=2, default=",")
|
csv_delim = models.CharField(max_length=2, default=",")
|
||||||
@ -515,7 +516,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def reset_inheritance(space=None):
|
def reset_inheritance(space=None):
|
||||||
# resets inheritted fields to the space defaults and updates all inheritted fields to root object values
|
# resets inherited fields to the space defaults and updates all inherited fields to root object values
|
||||||
inherit = space.food_inherit.all()
|
inherit = space.food_inherit.all()
|
||||||
|
|
||||||
# remove all inherited fields from food
|
# remove all inherited fields from food
|
||||||
|
@ -33,7 +33,7 @@ class Nextcloud(Provider):
|
|||||||
try:
|
try:
|
||||||
files.pop(0) # remove first element because its the folder itself
|
files.pop(0) # remove first element because its the folder itself
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass # folder is emtpy, no recipes will be imported
|
pass # folder is empty, no recipes will be imported
|
||||||
|
|
||||||
import_count = 0
|
import_count = 0
|
||||||
for file in files:
|
for file in files:
|
||||||
|
@ -183,7 +183,7 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
|
|||||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_kj', 'search_style', 'show_recent', 'plan_share',
|
||||||
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_inherit_default', 'default_delay',
|
'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',
|
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'csv_delim', 'csv_prefix',
|
||||||
'filter_to_supermarket', 'shopping_add_onhand'
|
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -865,7 +865,7 @@ class AutomationSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
# CORS, REST and Scopes aren't currently working
|
# CORS, REST and Scopes aren't currently working
|
||||||
# Scopes are evaluating before REST has authenticated the user assiging a None space
|
# Scopes are evaluating before REST has authenticated the user assigning a None space
|
||||||
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
|
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
|
||||||
class BookmarkletImportSerializer(serializers.ModelSerializer):
|
class BookmarkletImportSerializer(serializers.ModelSerializer):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
@ -72,7 +72,7 @@ def update_food_inheritance(sender, instance=None, created=False, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
inherit = inherit.values_list('field', flat=True)
|
inherit = inherit.values_list('field', flat=True)
|
||||||
# apply changes from parent to instance for each inheritted field
|
# apply changes from parent to instance for each inherited field
|
||||||
if instance.parent and inherit.count() > 0:
|
if instance.parent and inherit.count() > 0:
|
||||||
parent = instance.get_parent()
|
parent = instance.get_parent()
|
||||||
if 'ignore_shopping' in inherit:
|
if 'ignore_shopping' in inherit:
|
||||||
|
5
cookbook/static/themes/tandoor.min.css
vendored
5
cookbook/static/themes/tandoor.min.css
vendored
@ -10462,3 +10462,8 @@ textarea, input:not([type="submit"]):not([class="multiselect__input"]):not([clas
|
|||||||
.form-control-search {
|
.form-control-search {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
background: #b98766 !important;
|
||||||
|
}
|
@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<h2>{% trans 'Lists' %}</h2>
|
<h2>{% trans 'Lists' %}</h2>
|
||||||
{% trans 'Lists can ordered or unorderd. It is <b>important to leave a blank line before the list!</b>' %}
|
{% trans 'Lists can ordered or unordered. It is <b>important to leave a blank line before the list!</b>' %}
|
||||||
<pre class="intro-code code-block"><code>
|
<pre class="intro-code code-block"><code>
|
||||||
{% trans 'Ordered List' %}
|
{% trans 'Ordered List' %}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
<h4>{% trans 'Simple' %}</h4>
|
<h4>{% trans 'Simple' %}</h4>
|
||||||
<p> {% blocktrans %}
|
<p> {% blocktrans %}
|
||||||
Simple searches ignore punctuation and common words such as 'the', 'a', 'and'. And will treat seperate words as required.
|
Simple searches ignore punctuation and common words such as 'the', 'a', 'and'. And will treat separate words as required.
|
||||||
Searching for 'apple or flour' will return any recipe that includes both 'apple' and 'flour' anywhere in the fields that have been selected for a full text search.
|
Searching for 'apple or flour' will return any recipe that includes both 'apple' and 'flour' anywhere in the fields that have been selected for a full text search.
|
||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
<h4>{% trans 'Phrase' %}</h4>
|
<h4>{% trans 'Phrase' %}</h4>
|
||||||
@ -39,7 +39,7 @@
|
|||||||
<p> {% blocktrans %}
|
<p> {% blocktrans %}
|
||||||
Web searches simulate functionality found on many web search sites supporting special syntax.
|
Web searches simulate functionality found on many web search sites supporting special syntax.
|
||||||
Placing quotes around several words will convert those words into a phrase.
|
Placing quotes around several words will convert those words into a phrase.
|
||||||
'or' is recongized as searching for the word (or phrase) immediately before 'or' OR the word (or phrase) directly after.
|
'or' is recognized as searching for the word (or phrase) immediately before 'or' OR the word (or phrase) directly after.
|
||||||
'-' is recognized as searching for recipes that do not include the word (or phrase) that comes immediately after.
|
'-' is recognized as searching for recipes that do not include the word (or phrase) that comes immediately after.
|
||||||
For example searching for 'apple pie' or cherry -butter will return any recipe that includes the phrase 'apple pie' or the word 'cherry'
|
For example searching for 'apple pie' or cherry -butter will return any recipe that includes the phrase 'apple pie' or the word 'cherry'
|
||||||
in any field included in the full text search but exclude any recipe that has the word 'butter' in any field included.
|
in any field included in the full text search but exclude any recipe that has the word 'butter' in any field included.
|
||||||
@ -59,7 +59,7 @@
|
|||||||
{% blocktrans %}
|
{% blocktrans %}
|
||||||
Another approach to searching that also requires Postgresql is fuzzy search or trigram similarity. A trigram is a group of three consecutive characters.
|
Another approach to searching that also requires Postgresql is fuzzy search or trigram similarity. A trigram is a group of three consecutive characters.
|
||||||
For example searching for 'apple' will create x trigrams 'app', 'ppl', 'ple' and will create a score of how closely words match the generated trigrams.
|
For example searching for 'apple' will create x trigrams 'app', 'ppl', 'ple' and will create a score of how closely words match the generated trigrams.
|
||||||
One benefit of searching trigams is that a search for 'sandwich' will find mispelled words such as 'sandwhich' that would be missed by other methods.
|
One benefit of searching trigams is that a search for 'sandwich' will find misspelled words such as 'sandwhich' that would be missed by other methods.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@
|
|||||||
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
||||||
<div class="row" v-if="!onLine">
|
<div class="row" v-if="!onLine">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not syncronize.' %}</div>
|
<div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not synchronize.' %}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -634,7 +634,7 @@
|
|||||||
console.log('updating recipe', this.shopping_list.recipes[i])
|
console.log('updating recipe', this.shopping_list.recipes[i])
|
||||||
recipe_promises.push(this.$http.post("{% url 'api:shoppinglistrecipe-list' %}", this.shopping_list.recipes[i], {}).then((response) => {
|
recipe_promises.push(this.$http.post("{% url 'api:shoppinglistrecipe-list' %}", this.shopping_list.recipes[i], {}).then((response) => {
|
||||||
let old_id = this.shopping_list.recipes[i].id
|
let old_id = this.shopping_list.recipes[i].id
|
||||||
console.log("list recipe create respose ", response.body)
|
console.log("list recipe create response ", response.body)
|
||||||
this.$set(this.shopping_list.recipes, i, response.body)
|
this.$set(this.shopping_list.recipes, i, response.body)
|
||||||
for (let e of this.shopping_list.entries.filter(item => item.list_recipe === old_id)) {
|
for (let e of this.shopping_list.entries.filter(item => item.list_recipe === old_id)) {
|
||||||
console.log("found recipe updating ID")
|
console.log("found recipe updating ID")
|
||||||
|
@ -908,27 +908,21 @@
|
|||||||
// })
|
// })
|
||||||
},
|
},
|
||||||
searchUnits: function (query) {
|
searchUnits: function (query) {
|
||||||
let apiFactory = new ApiApiFactory()
|
|
||||||
|
|
||||||
this.units_loading = true
|
this.units_loading = true
|
||||||
apiFactory
|
this.$http.get("{% url 'dal_unit' %}" + '?q=' + query).then((response) => {
|
||||||
.listUnits(query, 1, this.options_limit)
|
this.units = response.data.results;
|
||||||
.then((response) => {
|
if (this.recipe_data !== undefined) {
|
||||||
this.units = response.data.results
|
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
||||||
|
if (x.unit !== null && x.unit.text !== '') {
|
||||||
if (this.recipe !== undefined) {
|
this.units = this.units.filter(item => item.text !== x.unit.text)
|
||||||
for (let s of this.recipe.steps) {
|
this.units.push(x.unit)
|
||||||
for (let i of s.ingredients) {
|
|
||||||
if (i.unit !== null && i.unit.id === undefined) {
|
|
||||||
this.units.push(i.unit)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.units_loading = false
|
this.units_loading = false
|
||||||
})
|
}).catch((err) => {
|
||||||
.catch((err) => {
|
console.log(err)
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
searchIngredients: function (query) {
|
searchIngredients: function (query) {
|
||||||
|
@ -1088,7 +1088,7 @@ def recipe_from_source(request):
|
|||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('No useable data could be found.')
|
'msg': _('No usable data could be found.')
|
||||||
},
|
},
|
||||||
status=400
|
status=400
|
||||||
)
|
)
|
||||||
|
@ -411,7 +411,7 @@ def user_settings(request):
|
|||||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||||
api_token = Token.objects.create(user=request.user)
|
api_token = Token.objects.create(user=request.user)
|
||||||
|
|
||||||
# these fields require postgress - just disable them if postgress isn't available
|
# these fields require postgresql - just disable them if postgresql isn't available
|
||||||
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||||
'django.db.backends.postgresql']:
|
'django.db.backends.postgresql']:
|
||||||
search_form.fields['search'].disabled = True
|
search_form.fields['search'].disabled = True
|
||||||
|
@ -51,7 +51,7 @@ In technical terms it is a multi tenant system.
|
|||||||
You can compare a space to something like google drive or dropbox.
|
You can compare a space to something like google drive or dropbox.
|
||||||
There is only one installation of the Dropbox system, but it handles multiple users without them noticing each other.
|
There is only one installation of the Dropbox system, but it handles multiple users without them noticing each other.
|
||||||
For Tandoor that means all people that work together on one recipe collection can be in one space.
|
For Tandoor that means all people that work together on one recipe collection can be in one space.
|
||||||
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (trough the admin interface).
|
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (through the admin interface).
|
||||||
|
|
||||||
Sharing between spaces is currently not possible but is planned for future releases.
|
Sharing between spaces is currently not possible but is planned for future releases.
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ SOCIAL_PROVIDERS=allauth.socialaccount.providers.github,allauth.socialaccount.pr
|
|||||||
The exact formatting is important so make sure to follow the steps explained here!
|
The exact formatting is important so make sure to follow the steps explained here!
|
||||||
|
|
||||||
Depending on your authentication provider you **might need** to configure it.
|
Depending on your authentication provider you **might need** to configure it.
|
||||||
This needs to be done trough the settings system. To make the system flexible (allow multiple providers) and to
|
This needs to be done through the settings system. To make the system flexible (allow multiple providers) and to
|
||||||
not require another file to be mounted into the container the configuration ins done trough a single
|
not require another file to be mounted into the container the configuration ins done through a single
|
||||||
environment variable. The downside of this approach is that the configuration needs to be put into a single line
|
environment variable. The downside of this approach is that the configuration needs to be put into a single line
|
||||||
as environment files loaded by docker compose don't support multiple lines for a single variable.
|
as environment files loaded by docker compose don't support multiple lines for a single variable.
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ The basic configuration is the same for all providers.
|
|||||||
### Local
|
### Local
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
There is currently no way to upload files trough the webinterface. This is a feature that might be added later.
|
There is currently no way to upload files through the webinterface. This is a feature that might be added later.
|
||||||
|
|
||||||
The local provider does not need any configuration.
|
The local provider does not need any configuration.
|
||||||
For the monitor you will need to define a valid path on your host system.
|
For the monitor you will need to define a valid path on your host system.
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continuous%20Integration/badge.svg?branch=master" ></a>
|
||||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||||
@ -85,7 +85,7 @@ there are some greater overall goals for the future (in no particular order)
|
|||||||
|
|
||||||
- Improve the UI! The Design is inconsistent and many pages work but don't look great. This needs to change.
|
- Improve the UI! The Design is inconsistent and many pages work but don't look great. This needs to change.
|
||||||
- I strongly believe in Open Data and Systems. Thus adding importers and exporters for all relevant other recipe management systems is something i really want to do.
|
- I strongly believe in Open Data and Systems. Thus adding importers and exporters for all relevant other recipe management systems is something i really want to do.
|
||||||
- Move all Javascript Libraries to a packet manger and clean up some of the mess I made in the early days
|
- Move all Javascript Libraries to a packet manager and clean up some of the mess I made in the early days
|
||||||
- Improve Test coverage and also the individual tests themselves
|
- Improve Test coverage and also the individual tests themselves
|
||||||
- Improve the documentation for all features and aspects of this project and add some application integrated help
|
- Improve the documentation for all features and aspects of this project and add some application integrated help
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
## K8s Setup
|
## K8s Setup
|
||||||
|
|
||||||
This is a setup which should be sufficent for production use. Be sure to replace the default secrets!
|
This is a setup which should be sufficient for production use. Be sure to replace the default secrets!
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ The creation of the persistent volume claims for media and static content. May y
|
|||||||
|
|
||||||
### 40-sts-postgresql.yaml
|
### 40-sts-postgresql.yaml
|
||||||
|
|
||||||
The PostgreSQL stateful set, based on a bitnami image. It runs a init container as root to do the preparations. The postgres container itsef runs as a lower privileged user. The recipes app uses the database super user (postgres) as the recipies app is doing some db migrations on startup, which needs super user privileges.
|
The PostgreSQL stateful set, based on a bitnami image. It runs a init container as root to do the preparations. The postgres container itself runs as a lower privileged user. The recipes app uses the database super user (postgres) as the recipes app is doing some db migrations on startup, which needs super user privileges.
|
||||||
|
|
||||||
### 45-service-db.yaml
|
### 45-service-db.yaml
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ Creating the database service.
|
|||||||
|
|
||||||
### 50-deployment.yaml
|
### 50-deployment.yaml
|
||||||
|
|
||||||
The deployment first fires up a init container to do the database migrations and file modifications. This init container runs as root. The init conainer runs part of the [boot.sh](https://github.com/TandoorRecipes/recipes/blob/develop/boot.sh) script from the `vabene1111/recipes` image.
|
The deployment first fires up a init container to do the database migrations and file modifications. This init container runs as root. The init container runs part of the [boot.sh](https://github.com/TandoorRecipes/recipes/blob/develop/boot.sh) script from the `vabene1111/recipes` image.
|
||||||
|
|
||||||
The deployment then runs two containers, the recipes-nginx and the recipes container which runs the gunicorn app. The nginx container gets it's nginx.conf via config map to deliver static content `/static` and `/media`. The guincorn container gets it's secret key and the database password from the secret `recipes`. `gunicorn` runs as user `nobody`.
|
The deployment then runs two containers, the recipes-nginx and the recipes container which runs the gunicorn app. The nginx container gets it's nginx.conf via config map to deliver static content `/static` and `/media`. The guincorn container gets it's secret key and the database password from the secret `recipes`. `gunicorn` runs as user `nobody`.
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Manual installation instructions
|
# Manual installation instructions
|
||||||
|
|
||||||
These intructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04))
|
These instructions are inspired from a standard django/gunicorn/postgresql instructions ([for example](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu-16-04))
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Be sure to use python 3.9 and pip related to python 3.9. Depending on your distribution calling `python` or `pip` will use python2 instead of python 3.9.
|
Be sure to use python 3.9 and pip related to python 3.9. Depending on your distribution calling `python` or `pip` will use python2 instead of python 3.9.
|
||||||
@ -122,7 +122,7 @@ ExecStart=/var/www/recipes/bin/gunicorn --error-logfile /tmp/gunicorn_err.log --
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
*Note*: `-error-logfile /tmp/gunicorn_err.log --log-level debug --capture-output` are usefull for debugging and can be removed later
|
*Note*: `-error-logfile /tmp/gunicorn_err.log --log-level debug --capture-output` are useful for debugging and can be removed later
|
||||||
|
|
||||||
*Note2*: Fix the path in the `ExecStart` line to where you gunicorn and recipes are
|
*Note2*: Fix the path in the `ExecStart` line to where you gunicorn and recipes are
|
||||||
|
|
||||||
|
@ -57,4 +57,4 @@ apache:
|
|||||||
|
|
||||||
I used two paths `<sub path>` and `<www path>` for simplicity. In my case I have `<sub path> = recipes` and `<www path> = serve/recipes`. One could also change the matching rules of traefik to have everything under one path.
|
I used two paths `<sub path>` and `<www path>` for simplicity. In my case I have `<sub path> = recipes` and `<www path> = serve/recipes`. One could also change the matching rules of traefik to have everything under one path.
|
||||||
|
|
||||||
I left out the TLS config in this example for simplicty.
|
I left out the TLS config in this example for simplicity.
|
||||||
|
@ -80,7 +80,7 @@ Easiest way is to do it via Reverse Proxy.
|
|||||||
- insert name
|
- insert name
|
||||||
- Source:
|
- Source:
|
||||||
- Protocol: HTTPS
|
- Protocol: HTTPS
|
||||||
- Hostname: URL if you acces from outside, otherwise ip in network
|
- Hostname: URL if you access from outside, otherwise ip in network
|
||||||
- Port: The port you want to access, has to be a different one that the one in the docker-compose file
|
- Port: The port you want to access, has to be a different one that the one in the docker-compose file
|
||||||
- HSTS can be enabled
|
- HSTS can be enabled
|
||||||
- Destination:
|
- Destination:
|
||||||
@ -90,14 +90,14 @@ Easiest way is to do it via Reverse Proxy.
|
|||||||
- Click on Custom Header and press Create -> Websocket
|
- Click on Custom Header and press Create -> Websocket
|
||||||
- Save
|
- Save
|
||||||
- Control Panel -> Security -> Firewall -> Edit Rules -> Create
|
- Control Panel -> Security -> Firewall -> Edit Rules -> Create
|
||||||
- Ports: Select form a list of build-in applications -> Select -> You find your Reverse Proxy, enable it
|
- Ports: Select form a list of built-in applications -> Select -> You find your Reverse Proxy, enable it
|
||||||
- Source IP: Depends, All allows access from outside, i use specific to only connect in my network
|
- Source IP: Depends, All allows access from outside, i use specific to only connect in my network
|
||||||
- Action: Allow
|
- Action: Allow
|
||||||
- Save and make sure it's above the deny rules
|
- Save and make sure it's above the deny rules
|
||||||
|
|
||||||
[Deprecated, Note: ssl Path changed for DSM 7]
|
[Deprecated, Note: ssl Path changed for DSM 7]
|
||||||
6.1 Additional SSL Setup
|
6.1 Additional SSL Setup
|
||||||
- create foler `ssl` inside `nginx` folder
|
- create folder `ssl` inside `nginx` folder
|
||||||
- download your ssl certificate from `security` tab in dsm `control panel`
|
- download your ssl certificate from `security` tab in dsm `control panel`
|
||||||
- or create a task in `task manager` because Synology will update the certificate every few months
|
- or create a task in `task manager` because Synology will update the certificate every few months
|
||||||
- set task to repeat every day
|
- set task to repeat every day
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
!!! danger
|
!!! danger
|
||||||
Please refer to [the offical documentation](https://doc.traefik.io/traefik/).
|
Please refer to [the official documentation](https://doc.traefik.io/traefik/).
|
||||||
This example just shows something similar to my setup in case you dont understand the offical documentation.
|
This example just shows something similar to my setup in case you dont understand the official documentation.
|
||||||
|
|
||||||
You need to create a network called `traefik` using `docker network create traefik`.
|
You need to create a network called `traefik` using `docker network create traefik`.
|
||||||
## docker-compose.yml
|
## docker-compose.yml
|
||||||
|
@ -2,7 +2,7 @@ There is currently no "good" way of backing up your data implemented in the appl
|
|||||||
This mean that you will be responsible for backing up your data.
|
This mean that you will be responsible for backing up your data.
|
||||||
|
|
||||||
It is planned to add a "real" backup feature similar to applications like homeassistant where a snapshot can be
|
It is planned to add a "real" backup feature similar to applications like homeassistant where a snapshot can be
|
||||||
downloaded and restored trough the web interface.
|
downloaded and restored through the web interface.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
When developing a new backup strategy, make sure to also test the restore process!
|
When developing a new backup strategy, make sure to also test the restore process!
|
||||||
|
@ -21,13 +21,13 @@ The following table roughly defines the capabilities of each role
|
|||||||
!!! warning
|
!!! warning
|
||||||
Users without groups cannot do anything. Make sure to assign them a group!
|
Users without groups cannot do anything. Make sure to assign them a group!
|
||||||
|
|
||||||
You can either create new users trough the admin interface or by sending them invite links.
|
You can either create new users through the admin interface or by sending them invite links.
|
||||||
|
|
||||||
Invite links can be generated on the System page. If you specify a username during the creation of the link
|
Invite links can be generated on the System page. If you specify a username during the creation of the link
|
||||||
the person using it won't be able to change that name.
|
the person using it won't be able to change that name.
|
||||||
|
|
||||||
## Managing Permissions
|
## Managing Permissions
|
||||||
Management of permissions can currently only be achieved trough the django admin interface.
|
Management of permissions can currently only be achieved through the django admin interface.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Please do not rename the groups as this breaks the permission system.
|
Please do not rename the groups as this breaks the permission system.
|
||||||
|
@ -407,7 +407,7 @@ TEST_RUNNER = "cookbook.helper.CustomTestRunner.CustomTestRunner"
|
|||||||
# settings for cross site origin (CORS)
|
# settings for cross site origin (CORS)
|
||||||
# all origins allowed to support bookmarklet
|
# all origins allowed to support bookmarklet
|
||||||
# all of this may or may not work with nginx or other web servers
|
# all of this may or may not work with nginx or other web servers
|
||||||
# TODO make this user configureable - enable or disable bookmarklets
|
# TODO make this user configurable - enable or disable bookmarklets
|
||||||
# TODO since token auth is enabled - this all should be https by default
|
# TODO since token auth is enabled - this all should be https by default
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
@ -79,21 +79,20 @@
|
|||||||
<div class="col-12 col-md-9 col-lg-6">
|
<div class="col-12 col-md-9 col-lg-6">
|
||||||
<h5>{{ $t("Meal_Types") }}</h5>
|
<h5>{{ $t("Meal_Types") }}</h5>
|
||||||
<div>
|
<div>
|
||||||
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10"
|
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" @sort="sortMealTypes()" ghost-class="ghost">
|
||||||
handle=".handle" @sort="sortMealTypes()">
|
<b-card no-body class="mt-1 list-group-item p-2" style="cursor:move" v-for="(meal_type, index) in meal_types" v-hover
|
||||||
<b-card no-body class="mt-1" v-for="(meal_type, index) in meal_types" v-hover
|
|
||||||
:key="meal_type.id">
|
:key="meal_type.id">
|
||||||
<b-card-header class="p-4">
|
<b-card-header class="p-2 border-0">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2 handle">
|
<div class="col-2">
|
||||||
<button type="button" class="btn btn-lg shadow-none"><i
|
<button type="button" class="btn btn-lg shadow-none"><i
|
||||||
class="fas fa-arrows-alt-v"></i></button>
|
class="fas fa-arrows-alt-v"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<h5>
|
<h5 class="mt-1 mb-1">
|
||||||
{{ meal_type.icon }} {{
|
{{ meal_type.icon }} {{
|
||||||
meal_type.name
|
meal_type.name
|
||||||
}}<span class="float-right text-primary"
|
}}<span class="float-right text-primary" style="cursor:pointer"
|
||||||
><i class="fa"
|
><i class="fa"
|
||||||
v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }"
|
v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }"
|
||||||
@click="editOrSaveMealType(index)" aria-hidden="true"></i
|
@click="editOrSaveMealType(index)" aria-hidden="true"></i
|
||||||
@ -878,4 +877,9 @@ having to override as much.
|
|||||||
.theme-default .cv-day.draghover {
|
.theme-default .cv-day.draghover {
|
||||||
box-shadow: inset 0 0 0.2em 0.2em yellow;
|
box-shadow: inset 0 0 0.2em 0.2em yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: #c8ebfb;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -8,15 +8,21 @@
|
|||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
|
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
|
||||||
<b-input-group>
|
<b-input-group>
|
||||||
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
<b-input
|
||||||
|
class="form-control form-control-lg form-control-borderless form-control-search"
|
||||||
|
v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
||||||
<b-input-group-append>
|
<b-input-group-append>
|
||||||
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()" v-if="debug && ui.sql_debug">
|
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()"
|
||||||
|
v-if="debug && ui.sql_debug">
|
||||||
<i class="fas fa-bug" style="font-size: 1.5em"></i>
|
<i class="fas fa-bug" style="font-size: 1.5em"></i>
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')" @click="openRandom()">
|
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')"
|
||||||
|
@click="openRandom()">
|
||||||
<i class="fas fa-dice-five" style="font-size: 1.5em"></i>
|
<i class="fas fa-dice-five" style="font-size: 1.5em"></i>
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover :title="$t('Advanced Settings')" v-bind:variant="!searchFiltered(true) ? 'primary' : 'danger'">
|
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover
|
||||||
|
:title="$t('Advanced Settings')"
|
||||||
|
v-bind:variant="!searchFiltered(true) ? 'primary' : 'danger'">
|
||||||
<!-- TODO consider changing this icon to a filter -->
|
<!-- TODO consider changing this icon to a filter -->
|
||||||
<i class="fas fa-caret-down" v-if="!search.advanced_search_visible"></i>
|
<i class="fas fa-caret-down" v-if="!search.advanced_search_visible"></i>
|
||||||
<i class="fas fa-caret-up" v-if="search.advanced_search_visible"></i>
|
<i class="fas fa-caret-up" v-if="search.advanced_search_visible"></i>
|
||||||
@ -26,15 +32,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="search.advanced_search_visible">
|
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm"
|
||||||
|
v-model="search.advanced_search_visible">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
|
<a class="btn btn-primary btn-block text-uppercase"
|
||||||
|
:href="resolveDjangoUrl('new_recipe')">{{ $t("New_Recipe") }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<a class="btn btn-primary btn-block text-uppercase" :href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
|
<a class="btn btn-primary btn-block text-uppercase"
|
||||||
|
:href="resolveDjangoUrl('data_import_url')">{{ $t("Import") }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<button
|
<button
|
||||||
@ -53,57 +62,92 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<button id="id_settings_button" class="btn btn-primary btn-block text-uppercase"><i class="fas fa-cog fa-lg m-1"></i></button>
|
<button id="id_settings_button"
|
||||||
|
class="btn btn-primary btn-block text-uppercase"><i
|
||||||
|
class="fas fa-cog fa-lg m-1"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-popover target="id_settings_button" triggers="click" placement="bottom">
|
<b-popover target="id_settings_button" triggers="click" placement="bottom">
|
||||||
<b-tabs content-class="mt-1" small>
|
<b-tabs content-class="mt-1" small>
|
||||||
<b-tab :title="$t('Settings')" active>
|
<b-tab :title="$t('Settings')" active>
|
||||||
<b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="6" class="mb-3">
|
<b-form-group v-bind:label="$t('Recently_Viewed')"
|
||||||
<b-form-input type="number" v-model="ui.recently_viewed" id="popover-input-1" size="sm"></b-form-input>
|
label-for="popover-input-1" label-cols="6" class="mb-3">
|
||||||
|
<b-form-input type="number" v-model="ui.recently_viewed"
|
||||||
|
id="popover-input-1" size="sm"></b-form-input>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
|
|
||||||
<b-form-group v-bind:label="$t('Recipes_per_page')" label-for="popover-input-page-count" label-cols="6" class="mb-3">
|
<b-form-group v-bind:label="$t('Recipes_per_page')"
|
||||||
<b-form-input type="number" v-model="ui.page_size" id="popover-input-page-count" size="sm"></b-form-input>
|
label-for="popover-input-page-count" label-cols="6"
|
||||||
|
class="mb-3">
|
||||||
|
<b-form-input type="number" v-model="ui.page_size"
|
||||||
|
id="popover-input-page-count"
|
||||||
|
size="sm"></b-form-input>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
|
|
||||||
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" label-cols="6" class="mb-3">
|
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2"
|
||||||
<b-form-checkbox switch v-model="ui.show_meal_plan" id="popover-input-2" size="sm"></b-form-checkbox>
|
label-cols="6" class="mb-3">
|
||||||
|
<b-form-checkbox switch v-model="ui.show_meal_plan"
|
||||||
|
id="popover-input-2" size="sm"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
|
|
||||||
<b-form-group v-if="ui.show_meal_plan" v-bind:label="$t('Meal_Plan_Days')" label-for="popover-input-5" label-cols="6" class="mb-3">
|
<b-form-group v-if="ui.show_meal_plan"
|
||||||
<b-form-input type="number" v-model="ui.meal_plan_days" id="popover-input-5" size="sm"></b-form-input>
|
v-bind:label="$t('Meal_Plan_Days')"
|
||||||
|
label-for="popover-input-5" label-cols="6" class="mb-3">
|
||||||
|
<b-form-input type="number" v-model="ui.meal_plan_days"
|
||||||
|
id="popover-input-5" size="sm"></b-form-input>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
|
|
||||||
<b-form-group v-bind:label="$t('Sort_by_new')" label-for="popover-input-3" label-cols="6" class="mb-3">
|
<b-form-group v-bind:label="$t('Sort_by_new')"
|
||||||
<b-form-checkbox switch v-model="ui.sort_by_new" id="popover-input-3" size="sm"></b-form-checkbox>
|
label-for="popover-input-3" label-cols="6" class="mb-3">
|
||||||
|
<b-form-checkbox switch v-model="ui.sort_by_new"
|
||||||
|
id="popover-input-3" size="sm"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<div class="row" style="margin-top: 1vh">
|
<div class="row" style="margin-top: 1vh">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Search Settings") }}</a>
|
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{
|
||||||
|
$t("Search Settings")
|
||||||
|
}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<b-tab title="Expert Settings">
|
<b-tab title="Expert Settings">
|
||||||
<b-form-group v-bind:label="$t('remember_search')" label-for="popover-rem-search" label-cols="6" class="mb-3">
|
<b-form-group v-bind:label="$t('remember_search')"
|
||||||
<b-form-checkbox switch v-model="ui.remember_search" id="popover-rem-search" size="sm"></b-form-checkbox>
|
label-for="popover-rem-search" label-cols="6"
|
||||||
|
class="mb-3">
|
||||||
|
<b-form-checkbox switch v-model="ui.remember_search"
|
||||||
|
id="popover-rem-search"
|
||||||
|
size="sm"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group v-if="ui.remember_search" v-bind:label="$t('remember_hours')" label-for="popover-input-rem-hours" label-cols="6" class="mb-3">
|
<b-form-group v-if="ui.remember_search"
|
||||||
<b-form-input type="number" v-model="ui.remember_hours" id="popover-rem-hours" size="sm"></b-form-input>
|
v-bind:label="$t('remember_hours')"
|
||||||
|
label-for="popover-input-rem-hours" label-cols="6"
|
||||||
|
class="mb-3">
|
||||||
|
<b-form-input type="number" v-model="ui.remember_hours"
|
||||||
|
id="popover-rem-hours" size="sm"></b-form-input>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group v-bind:label="$t('tree_select')" label-for="popover-input-treeselect" label-cols="6" class="mb-3">
|
<b-form-group v-bind:label="$t('tree_select')"
|
||||||
<b-form-checkbox switch v-model="ui.tree_select" id="popover-input-treeselect" size="sm"></b-form-checkbox>
|
label-for="popover-input-treeselect" label-cols="6"
|
||||||
|
class="mb-3">
|
||||||
|
<b-form-checkbox switch v-model="ui.tree_select"
|
||||||
|
id="popover-input-treeselect"
|
||||||
|
size="sm"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group v-if="debug" v-bind:label="$t('sql_debug')" label-for="popover-input-sqldebug" label-cols="6" class="mb-3">
|
<b-form-group v-if="debug" v-bind:label="$t('sql_debug')"
|
||||||
<b-form-checkbox switch v-model="ui.sql_debug" id="popover-input-sqldebug" size="sm"></b-form-checkbox>
|
label-for="popover-input-sqldebug" label-cols="6"
|
||||||
|
class="mb-3">
|
||||||
|
<b-form-checkbox switch v-model="ui.sql_debug"
|
||||||
|
id="popover-input-sqldebug"
|
||||||
|
size="sm"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
|
|
||||||
<div class="row" style="margin-top: 1vh">
|
<div class="row" style="margin-top: 1vh">
|
||||||
<div class="col-12" style="text-align: right">
|
<div class="col-12" style="text-align: right">
|
||||||
<b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
|
<b-button size="sm" variant="secondary" style="margin-right: 8px"
|
||||||
|
@click="$root.$emit('bv::hide::popover')">{{ $t("Close") }}
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-popover>
|
</b-popover>
|
||||||
@ -138,8 +182,12 @@
|
|||||||
></generic-multiselect>
|
></generic-multiselect>
|
||||||
<b-input-group-append>
|
<b-input-group-append>
|
||||||
<b-input-group-text>
|
<b-input-group-text>
|
||||||
<b-form-checkbox v-model="search.search_keywords_or" name="check-button" @change="refreshData(false)" class="shadow-none" switch>
|
<b-form-checkbox v-model="search.search_keywords_or"
|
||||||
<span class="text-uppercase" v-if="search.search_keywords_or">{{ $t("or") }}</span>
|
name="check-button"
|
||||||
|
@change="refreshData(false)"
|
||||||
|
class="shadow-none" switch>
|
||||||
|
<span class="text-uppercase"
|
||||||
|
v-if="search.search_keywords_or">{{ $t("or") }}</span>
|
||||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||||
</b-form-checkbox>
|
</b-form-checkbox>
|
||||||
</b-input-group-text>
|
</b-input-group-text>
|
||||||
@ -178,8 +226,13 @@
|
|||||||
></generic-multiselect>
|
></generic-multiselect>
|
||||||
<b-input-group-append>
|
<b-input-group-append>
|
||||||
<b-input-group-text>
|
<b-input-group-text>
|
||||||
<b-form-checkbox v-model="search.search_foods_or" name="check-button" @change="refreshData(false)" class="shadow-none" switch>
|
<b-form-checkbox v-model="search.search_foods_or"
|
||||||
<span class="text-uppercase" v-if="search.search_foods_or">{{ $t("or") }}</span>
|
name="check-button"
|
||||||
|
@change="refreshData(false)"
|
||||||
|
class="shadow-none" switch>
|
||||||
|
<span class="text-uppercase" v-if="search.search_foods_or">{{
|
||||||
|
$t("or")
|
||||||
|
}}</span>
|
||||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||||
</b-form-checkbox>
|
</b-form-checkbox>
|
||||||
</b-input-group-text>
|
</b-input-group-text>
|
||||||
@ -203,8 +256,13 @@
|
|||||||
></generic-multiselect>
|
></generic-multiselect>
|
||||||
<b-input-group-append>
|
<b-input-group-append>
|
||||||
<b-input-group-text>
|
<b-input-group-text>
|
||||||
<b-form-checkbox v-model="search.search_books_or" name="check-button" @change="refreshData(false)" class="shadow-none" tyle="width: 100%" switch>
|
<b-form-checkbox v-model="search.search_books_or"
|
||||||
<span class="text-uppercase" v-if="search.search_books_or">{{ $t("or") }}</span>
|
name="check-button"
|
||||||
|
@change="refreshData(false)"
|
||||||
|
class="shadow-none" tyle="width: 100%" switch>
|
||||||
|
<span class="text-uppercase" v-if="search.search_books_or">{{
|
||||||
|
$t("or")
|
||||||
|
}}</span>
|
||||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||||
</b-form-checkbox>
|
</b-form-checkbox>
|
||||||
</b-input-group-text>
|
</b-input-group-text>
|
||||||
@ -241,7 +299,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12 text-right" style="margin-top: 2vh">
|
<div class="col col-md-12 text-right" style="margin-top: 2vh">
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
{{ $t("Page") }} {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }}
|
{{ $t("Page") }} {{ search.pagination_page }}/{{
|
||||||
|
Math.ceil(pagination_count / ui.page_size)
|
||||||
|
}}
|
||||||
<a href="#" @click="resetSearch"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
|
<a href="#" @click="resetSearch"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -249,18 +309,24 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
|
<div
|
||||||
|
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
|
||||||
<template v-if="!searchFiltered()">
|
<template v-if="!searchFiltered()">
|
||||||
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe" :meal_plan="m" :footer_text="m.meal_type_name" footer_icon="far fa-calendar-alt"></recipe-card>
|
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe"
|
||||||
|
:meal_plan="m" :footer_text="m.meal_type_name"
|
||||||
|
footer_icon="far fa-calendar-alt"></recipe-card>
|
||||||
</template>
|
</template>
|
||||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" :footer_text="isRecentOrNew(r)[0]" :footer_icon="isRecentOrNew(r)[1]"> </recipe-card>
|
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r"
|
||||||
|
:footer_text="isRecentOrNew(r)[0]"
|
||||||
|
:footer_icon="isRecentOrNew(r)[1]"></recipe-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" style="margin-top: 2vh" v-if="!random_search">
|
<div class="row" style="margin-top: 2vh" v-if="!random_search">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count" :per-page="ui.page_size" @change="pageChange" align="center"> </b-pagination>
|
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count"
|
||||||
|
:per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -369,26 +435,44 @@ export default {
|
|||||||
}
|
}
|
||||||
let urlParams = new URLSearchParams(window.location.search)
|
let urlParams = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
|
|
||||||
if (urlParams.has("keyword")) {
|
if (urlParams.has("keyword")) {
|
||||||
this.search.search_keywords = []
|
this.search.search_keywords = []
|
||||||
this.facets.Keywords = []
|
this.facets.Keywords = []
|
||||||
for (let x of urlParams.getAll("keyword")) {
|
for (let x of urlParams.getAll("keyword")) {
|
||||||
this.search.search_keywords.push(Number.parseInt(x))
|
let initial_keyword = {id: Number.parseInt(x), name: "loading..."}
|
||||||
this.facets.Keywords.push({ id: x, name: "loading..." })
|
this.search.search_keywords.push(initial_keyword)
|
||||||
|
|
||||||
|
this.genericAPI(this.Models.KEYWORD, this.Actions.FETCH, {id: initial_keyword.id}).then((response) => {
|
||||||
|
let kw_index = this.search.search_keywords.findIndex((k => k.id === initial_keyword.id))
|
||||||
|
this.$set(this.search.search_keywords, kw_index, response.data)
|
||||||
|
this.$set(this.facets.Keywords, kw_index, response.data)
|
||||||
|
}).catch((err) => {
|
||||||
|
if (err.response.status === 404) {
|
||||||
|
let kw_index = this.search.search_keywords.findIndex((k => k.id === initial_keyword.id))
|
||||||
|
this.search.search_keywords.splice(kw_index, 1)
|
||||||
|
this.facets.Keywords.splice(kw_index, 1)
|
||||||
|
this.refreshData(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.facets.Foods = []
|
this.facets.Foods = []
|
||||||
for (let x of this.search.search_foods) {
|
for (let x of this.search.search_foods) {
|
||||||
this.facets.Foods.push({id: x, name: "loading..."})
|
this.facets.Foods.push({id: x, name: "loading..."})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.facets.Keywords = []
|
this.facets.Keywords = []
|
||||||
for (let x of this.search.search_keywords) {
|
for (let x of this.search.search_keywords) {
|
||||||
this.facets.Keywords.push({id: x, name: "loading..."})
|
this.facets.Keywords.push({id: x, name: "loading..."})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.facets.Books = []
|
this.facets.Books = []
|
||||||
for (let x of this.search.search_books) {
|
for (let x of this.search.search_books) {
|
||||||
this.facets.Books.push({id: x, name: "loading..."})
|
this.facets.Books.push({id: x, name: "loading..."})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadMealPlan()
|
this.loadMealPlan()
|
||||||
this.refreshData(false)
|
this.refreshData(false)
|
||||||
})
|
})
|
||||||
@ -532,7 +616,8 @@ export default {
|
|||||||
},
|
},
|
||||||
showSQL: function () {
|
showSQL: function () {
|
||||||
let params = this.buildParams()
|
let params = this.buildParams()
|
||||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {})
|
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
|
||||||
|
})
|
||||||
},
|
},
|
||||||
// TODO refactor to combine with load KeywordChildren
|
// TODO refactor to combine with load KeywordChildren
|
||||||
loadFoodChildren({action, parentNode, callback}) {
|
loadFoodChildren({action, parentNode, callback}) {
|
||||||
|
@ -4,26 +4,20 @@
|
|||||||
<div class="row float-top pl-0 pr-0">
|
<div class="row float-top pl-0 pr-0">
|
||||||
<div class="col-auto no-gutter ml-auto">
|
<div class="col-auto no-gutter ml-auto">
|
||||||
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
|
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
|
||||||
<i class="btn fas fa-plus-circle fa-lg px-0" @click="entrymode = !entrymode"
|
<i class="btn fas fa-plus-circle fa-lg px-0" @click="entrymode = !entrymode" :class="entrymode ? 'text-success' : 'text-primary'" />
|
||||||
:class="entrymode ? 'text-success' : 'text-muted'"/>
|
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
|
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
|
||||||
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-muted px-1" id="downloadShoppingLink"
|
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-primary px-1" id="downloadShoppingLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
||||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
|
||||||
|
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="downloadShoppingLink">
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="downloadShoppingLink">
|
||||||
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||||
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')"
|
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
||||||
icon="fas fa-file-csv"/>
|
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')" icon="fas fa-clipboard-list" />
|
||||||
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')"
|
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')" icon="fab fa-markdown" />
|
||||||
icon="fas fa-clipboard-list"/>
|
|
||||||
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')"
|
|
||||||
icon="fab fa-markdown"/>
|
|
||||||
</div>
|
</div>
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="link" id="id_filters_button" class="px-1 pt-0 pb-1">
|
<b-button variant="link" id="id_filters_button" class="px-1 pt-0 pb-1">
|
||||||
<i class="btn fas fa-filter text-decoration-none fa-lg px-1"
|
<i class="btn fas fa-filter text-decoration-none fa-lg px-1" :class="filterApplied ? 'text-danger' : 'text-primary'" />
|
||||||
:class="filterApplied ? 'text-danger' : 'text-muted'"/>
|
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -35,7 +29,7 @@
|
|||||||
<b-spinner v-if="loading" type="border" small></b-spinner>
|
<b-spinner v-if="loading" type="border" small></b-spinner>
|
||||||
{{ $t("Shopping_list") }}
|
{{ $t("Shopping_list") }}
|
||||||
</template>
|
</template>
|
||||||
<div class="container p-0 pr-lg-5 pl-lg-5" id="shoppinglist">
|
<div class="container p-0" id="shoppinglist">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12 p-0 p-lg-3">
|
<div class="col col-md-12 p-0 p-lg-3">
|
||||||
<div role="tablist">
|
<div role="tablist">
|
||||||
@ -43,23 +37,23 @@
|
|||||||
|
|
||||||
<b-row class="justify-content-md-center align-items-center pl-1 pr-1" v-if="entrymode">
|
<b-row class="justify-content-md-center align-items-center pl-1 pr-1" v-if="entrymode">
|
||||||
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-none d-md-block mt-1">
|
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-none d-md-block mt-1">
|
||||||
<b-form-input size="lg" min="1" type="number" :description="$t('Amount')"
|
<b-form-input
|
||||||
|
size="lg"
|
||||||
|
min="1"
|
||||||
|
type="number"
|
||||||
|
:description="$t('Amount')"
|
||||||
v-model="new_item.amount"
|
v-model="new_item.amount"
|
||||||
style="font-size: 16px;border-radius: 5px !important;border: 1px solid #e8e8e8 !important;"></b-form-input>
|
style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important"
|
||||||
|
></b-form-input>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1">
|
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1">
|
||||||
<lookup-input :class_list="'mb-0'" :form="formUnit" :model="Models.UNIT"
|
<lookup-input :class_list="'mb-0'" :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" :clear="clear" />
|
||||||
@change="new_item.unit = $event"
|
|
||||||
:show_label="false" :clear="clear"/>
|
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1">
|
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1">
|
||||||
<lookup-input :class_list="'mb-0'" :form="formFood" :model="Models.FOOD"
|
<lookup-input :class_list="'mb-0'" :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" :clear="clear" />
|
||||||
@change="new_item.food = $event"
|
|
||||||
:show_label="false" :clear="clear"/>
|
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" md="11" v-if="entry_mode_simple" class="mt-1">
|
<b-col cols="12" md="11" v-if="entry_mode_simple" class="mt-1">
|
||||||
<b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient"
|
<b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient" @keyup.enter="addItem"></b-form-input>
|
||||||
@keyup.enter="addItem"></b-form-input>
|
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" md="1" class="d-none d-md-block mt-1">
|
<b-col cols="12" md="1" class="d-none d-md-block mt-1">
|
||||||
<b-button variant="link" class="px-0">
|
<b-button variant="link" class="px-0">
|
||||||
@ -69,9 +63,14 @@
|
|||||||
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-block d-md-none mt-1">
|
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-block d-md-none mt-1">
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col cols="9">
|
<b-col cols="9">
|
||||||
<b-form-input size="lg" min="1" type="number" :description="$t('Amount')"
|
<b-form-input
|
||||||
|
size="lg"
|
||||||
|
min="1"
|
||||||
|
type="number"
|
||||||
|
:description="$t('Amount')"
|
||||||
v-model="new_item.amount"
|
v-model="new_item.amount"
|
||||||
style="font-size: 16px;border-radius: 5px !important;border: 1px solid #e8e8e8 !important;"></b-form-input>
|
style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important"
|
||||||
|
></b-form-input>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="3" class="flex-grow-1">
|
<b-col cols="3" class="flex-grow-1">
|
||||||
<b-button variant="success" class="p-0 pt-1 w-100 h-100">
|
<b-button variant="success" class="p-0 pt-1 w-100 h-100">
|
||||||
@ -93,19 +92,19 @@
|
|||||||
<!-- shopping list table -->
|
<!-- shopping list table -->
|
||||||
<div v-if="items && items.length > 0">
|
<div v-if="items && items.length > 0">
|
||||||
<div v-for="(done, x) in Sections" :key="x">
|
<div v-for="(done, x) in Sections" :key="x">
|
||||||
<div v-if="x == 'true'">
|
<div v-if="x == 'true'" class="bg-header w-100 text-center d-flex justify-content-center align-items-center">
|
||||||
<h4 class="pl-2 pl-md-0">{{ $t("Completed") }}</h4>
|
<span class="h4 d-flex mt-1 mb-1">{{ $t("Completed") }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="(s, i) in done" :key="i">
|
<div v-for="(s, i) in done" :key="i">
|
||||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" v-if="Object.entries(s).length > 0">
|
||||||
v-if="Object.entries(s).length > 0">
|
|
||||||
<button
|
<button
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn dropdown-toggle btn-link text-decoration-none text-dark pr-2 dropdown-toggle-no-caret"
|
class="btn dropdown-toggle btn-link text-decoration-none text-dark pr-2 dropdown-toggle-no-caret"
|
||||||
@click.stop="openContextMenu($event, s, true)">
|
@click.stop="openContextMenu($event, s, true)"
|
||||||
|
>
|
||||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -121,15 +120,19 @@
|
|||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel"
|
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
||||||
:class="{ show: x == 'false' }">
|
|
||||||
<!-- passing an array of values to the table grouped by Food -->
|
<!-- passing an array of values to the table grouped by Food -->
|
||||||
<transition-group name="slider-fade" mode="out-in">
|
<transition-group name="slide-fade">
|
||||||
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
||||||
|
<transition name="slide-fade" mode="out-in">
|
||||||
<ShoppingLineItem :entries="entries[1]" :groupby="group_by"
|
<ShoppingLineItem
|
||||||
@open-context-menu="openContextMenu" @update-checkbox="updateChecked"/>
|
:entries="entries[1]"
|
||||||
|
:groupby="group_by"
|
||||||
|
:settings="settings"
|
||||||
|
@open-context-menu="openContextMenu"
|
||||||
|
@update-checkbox="updateChecked"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
@ -143,6 +146,35 @@
|
|||||||
</b-tab>
|
</b-tab>
|
||||||
<!-- recipe tab -->
|
<!-- recipe tab -->
|
||||||
<b-tab :title="$t('Recipes')">
|
<b-tab :title="$t('Recipes')">
|
||||||
|
<div class="container p-0">
|
||||||
|
<b-row class="justify-content-md-center align-items-center p-1">
|
||||||
|
<b-col cols="10">
|
||||||
|
<b-input-group>
|
||||||
|
<b-input-group-prepend is-text>
|
||||||
|
{{ $t("Servings") }}
|
||||||
|
</b-input-group-prepend>
|
||||||
|
<b-input-group-prepend is-text>
|
||||||
|
<input type="number" :min="1" v-model="add_recipe_servings" style="width: 3em" />
|
||||||
|
</b-input-group-prepend>
|
||||||
|
<!-- <b-input-group-prepend is-text>
|
||||||
|
<b>{{ $t("Recipe") }}</b>
|
||||||
|
</b-input-group-prepend> -->
|
||||||
|
<generic-multiselect
|
||||||
|
class="input-group-text m-0 p-0"
|
||||||
|
@change="new_recipe = $event.val"
|
||||||
|
:label="'name'"
|
||||||
|
:model="Models.RECIPE"
|
||||||
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
|
v-bind:placeholder="$t('Recipe')"
|
||||||
|
:limit="20"
|
||||||
|
:multiple="false"
|
||||||
|
/>
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="success" @click="addRecipeToShopping" :disabled="!new_recipe.id">{{ $t("Add_to_Shopping") }}</b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
<table class="table w-100">
|
<table class="table w-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -156,22 +188,21 @@
|
|||||||
<td>{{ r.recipe_mealplan.name }}</td>
|
<td>{{ r.recipe_mealplan.name }}</td>
|
||||||
<td>{{ r.recipe_mealplan.recipe_name }}</td>
|
<td>{{ r.recipe_mealplan.recipe_name }}</td>
|
||||||
<td class="block-inline">
|
<td class="block-inline">
|
||||||
<b-form-input min="1" type="number" :debounce="300" :value="r.recipe_mealplan.servings"
|
<b-form-input min="1" type="number" :debounce="300" :value="r.recipe_mealplan.servings" @input="updateServings($event, r.list_recipe)"></b-form-input>
|
||||||
@input="updateServings($event, r.list_recipe)"></b-form-input>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<i class="btn text-danger fas fa-trash fa-lg px-2 border-0" variant="link" :title="$t('Delete')"
|
<i class="btn text-danger fas fa-trash fa-lg px-2 border-0" variant="link" :title="$t('Delete')" @click="deleteRecipe($event, r.list_recipe)" />
|
||||||
@click="deleteRecipe($event, r.list_recipe)"/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<!-- supermarkets tab -->
|
<!-- supermarkets tab -->
|
||||||
<b-tab :title="$t('Supermarkets')">
|
<b-tab :title="$t('Supermarkets')">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<!-- supermarkets column -->
|
<!-- supermarkets column -->
|
||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
<b-card>
|
<b-card no-body>
|
||||||
<template #header>
|
<template #header>
|
||||||
<h4 class="mb-0">
|
<h4 class="mb-0">
|
||||||
{{ $t("Supermarkets") }}
|
{{ $t("Supermarkets") }}
|
||||||
@ -188,77 +219,76 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<i class="btn fas fa-plus-circle fa-lg px-0"
|
<i class="btn fas fa-plus-circle fa-lg px-0" :class="new_supermarket.entrymode ? 'text-success' : 'text-primary'" />
|
||||||
:class="new_supermarket.entrymode ? 'text-success' : 'text-muted'"/>
|
|
||||||
</b-button>
|
</b-button>
|
||||||
</h4>
|
</h4>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<b-card
|
<b-card
|
||||||
class="m-1 p-1 no-body"
|
class="pt-5 pl-5 pr-5"
|
||||||
border-variant="success"
|
border-variant="success"
|
||||||
header-bg-variant="success"
|
header-bg-variant="success"
|
||||||
header-text-variant="white"
|
header-text-variant="white"
|
||||||
align="center"
|
align="center"
|
||||||
v-if="new_supermarket.entrymode"
|
v-if="new_supermarket.entrymode"
|
||||||
:header="$t('SupermarketName')"
|
:header="new_supermarket.value ? new_supermarket.value : $t('SupermarketName')"
|
||||||
>
|
>
|
||||||
<div class="input-group">
|
<b-input-group>
|
||||||
<b-form-input type="text" :placeholder="$t('SupermarketName')" v-model="new_supermarket.value"/>
|
<b-form-input type="text" class="form-control-append" :placeholder="$t('SupermarketName')" v-model="new_supermarket.value" />
|
||||||
<b-button class="input-group-append" variant="success" @click="addSupermarket"><i
|
<b-input-group-append>
|
||||||
class="pr-2 pt-1 fas fa-save"></i> {{ $t("Save") }}
|
<b-button class="input-group-append" variant="success" @click="addSupermarket"><i class="pr-2 pt-1 fas fa-save"></i> {{ $t("Create") }} </b-button>
|
||||||
</b-button>
|
</b-input-group-append>
|
||||||
</div>
|
</b-input-group>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
|
||||||
<b-card-body class="m-0 p-0">
|
<b-card-body class="m-0 p-0">
|
||||||
<b-card class="no-body mb-2" v-for="s in supermarkets" v-bind:key="s.id">
|
<b-card class="mt-1 p-0" v-for="s in supermarkets" v-bind:key="s.id">
|
||||||
<b-card-title>
|
<b-card-header class="p-2 border-0 pt-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">{{ s.name }}</div>
|
<div class="col-12">
|
||||||
<div class="col-auto text-right ml-auto">
|
<h5 class="mt-1 mb-1">
|
||||||
<b-button variant="link"
|
{{ s.name }}
|
||||||
class="p-0 m-0"
|
|
||||||
@click="s.editmode = !s.editmode;new_category.entrymode = false;new_supermarket.entrymode = false;editSupermarket(s)">
|
|
||||||
<i class="btn fas fa-edit fa-lg px-0" :class="s.editmode ? 'text-success' : 'text-muted'"/>
|
|
||||||
</b-button>
|
|
||||||
<b-button variant="link" class="p-0 m-0" @click="deleteSupermarket(s)">
|
|
||||||
<i class="btn fas fa-trash fa-lg px-2 text-muted"/>
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-card-title>
|
|
||||||
<b-card-body class="py-0">
|
|
||||||
<generic-pill :item_list="s.category_to_supermarket" label="category::name"
|
|
||||||
color="info"></generic-pill>
|
|
||||||
</b-card-body>
|
|
||||||
</b-card>
|
|
||||||
</b-card-body>
|
|
||||||
</b-card>
|
|
||||||
</div>
|
|
||||||
<!-- supermarket category column -->
|
|
||||||
<div class="col col-md-5">
|
|
||||||
<b-card class="no-body">
|
|
||||||
<template #header>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
{{ $t("Shopping_Categories") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto text-right ml-auto">
|
|
||||||
<b-button
|
<b-button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="p-0 m-0"
|
class="p-0 m-0 float-right"
|
||||||
|
@click="
|
||||||
|
s.editmode = !s.editmode
|
||||||
|
new_category.entrymode = false
|
||||||
|
new_supermarket.entrymode = false
|
||||||
|
editSupermarket(s)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="btn fas fa-edit fa-lg px-0" :class="s.editmode ? 'text-success' : 'text-primary'" />
|
||||||
|
</b-button>
|
||||||
|
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteSupermarket(s)">
|
||||||
|
<i class="btn fas fa-trash fa-lg px-2 text-danger" />
|
||||||
|
</b-button>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-card-header>
|
||||||
|
<b-card-body class="m-0 p-0">
|
||||||
|
<generic-pill :item_list="s.category_to_supermarket" label="category::name" color="info"></generic-pill>
|
||||||
|
</b-card-body>
|
||||||
|
</b-card>
|
||||||
|
</b-card-body>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-5">
|
||||||
|
<b-card>
|
||||||
|
<template #header>
|
||||||
|
<h4 class="mb-0">
|
||||||
|
{{ $t("Shopping_Categories") }}
|
||||||
|
<b-button
|
||||||
|
variant="link"
|
||||||
|
class="p-0 m-0 float-right"
|
||||||
@click="
|
@click="
|
||||||
new_category.entrymode = !new_category.entrymode
|
new_category.entrymode = !new_category.entrymode
|
||||||
new_supermarket.entrymode = false
|
new_supermarket.entrymode = false
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<i class="btn fas fa-plus-circle fa-lg px-0"
|
<i class="btn fas fa-plus-circle fa-lg px-0" :class="new_category.entrymode ? 'text-success' : 'text-primary'" />
|
||||||
:class="new_category.entrymode ? 'text-success' : 'text-muted'"/>
|
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</h4>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<b-card
|
<b-card
|
||||||
class="m-1 p-1 no-body"
|
class="m-1 p-1 no-body"
|
||||||
@ -267,22 +297,18 @@
|
|||||||
header-text-variant="white"
|
header-text-variant="white"
|
||||||
align="center"
|
align="center"
|
||||||
v-if="new_category.entrymode"
|
v-if="new_category.entrymode"
|
||||||
:header="$t('CategoryName')"
|
:header="new_category.value ? new_category.value : $t('CategoryName')"
|
||||||
>
|
>
|
||||||
<div class="input-group">
|
<b-input-group>
|
||||||
<b-form-input type="text" :placeholder="$t('CategoryName')" v-model="new_category.value"/>
|
<b-form-input type="text" class="form-control-append" :placeholder="$t('CategoryName')" v-model="new_category.value" />
|
||||||
<b-button class="input-group-append" variant="success" @click="addCategory"><i
|
<b-input-group-append>
|
||||||
class="pr-2 pt-1 fas fa-save"></i> {{ $t("Save") }}
|
<b-button class="input-group-append" variant="success" @click="addCategory"><i class="pr-2 pt-1 fas fa-save"></i> {{ $t("Create") }} </b-button>
|
||||||
</b-button>
|
</b-input-group-append>
|
||||||
</div>
|
</b-input-group>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
|
||||||
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3">{{
|
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3">{{ $t("CategoryInstruction") }} </b-card-sub-title>
|
||||||
$t("CategoryInstruction")
|
<b-card v-if="new_supermarket.editmode && supermarketCategory.length === 0" class="m-0 p-0 font-weight-bold no-body" border-variant="success" v-bind:key="-1" />
|
||||||
}}
|
|
||||||
</b-card-sub-title>
|
|
||||||
<b-card v-if="new_supermarket.editmode && supermarketCategory.length === 0"
|
|
||||||
class="m-0 p-0 font-weight-bold no-body" border-variant="success" v-bind:key="-1"/>
|
|
||||||
<draggable
|
<draggable
|
||||||
class="list-group"
|
class="list-group"
|
||||||
:list="supermarketCategory"
|
:list="supermarketCategory"
|
||||||
@ -295,39 +321,61 @@
|
|||||||
>
|
>
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<b-card
|
<b-card
|
||||||
class="m-0 p-0 font-weight-bold no-body list-group-item"
|
no-body
|
||||||
|
v-hover
|
||||||
|
class="mt-1 list-group-item p-2"
|
||||||
:style="new_supermarket.editmode ? 'cursor:move' : ''"
|
:style="new_supermarket.editmode ? 'cursor:move' : ''"
|
||||||
v-for="c in supermarketCategory"
|
v-for="c in supermarketCategory"
|
||||||
v-bind:key="c.id"
|
v-bind:key="c.id"
|
||||||
:border-variant="new_supermarket.editmode ? 'success' : ''"
|
:border-variant="new_supermarket.editmode ? 'success' : ''"
|
||||||
>
|
>
|
||||||
|
<b-card-header class="p-2 border-0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2" v-if="new_supermarket.editmode">
|
||||||
|
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||||
|
</div>
|
||||||
|
<div :class="new_supermarket.editmode ? 'col-10' : 'col-12'">
|
||||||
|
<h5 class="mt-1 mb-1">
|
||||||
{{ categoryName(c) }}
|
{{ categoryName(c) }}
|
||||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
||||||
<i class="btn fas fa-trash fa-lg px-2 text-muted"/>
|
<i class="btn fas fa-trash fa-lg px-2 text-danger" />
|
||||||
</b-button>
|
</b-button>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-card-header>
|
||||||
</b-card>
|
</b-card>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</draggable>
|
</draggable>
|
||||||
<hr style="height: 2px; background-color: black" v-if="new_supermarket.editmode" />
|
<hr style="height: 2px; background-color: black" v-if="new_supermarket.editmode" />
|
||||||
<b-card v-if="new_supermarket.editmode && notSupermarketCategory.length === 0" v-bind:key="-2"
|
<b-card v-if="new_supermarket.editmode && notSupermarketCategory.length === 0" v-bind:key="-2" class="m-0 p-0 font-weight-bold no-body" border-variant="danger" />
|
||||||
class="m-0 p-0 font-weight-bold no-body" border-variant="danger"/>
|
|
||||||
<draggable
|
<draggable
|
||||||
class="list-group"
|
class="list-group"
|
||||||
:list="notSupermarketCategory"
|
:list="notSupermarketCategory"
|
||||||
group="category"
|
group="category"
|
||||||
v-if="new_supermarket.editmode"
|
|
||||||
@start="drag = true"
|
@start="drag = true"
|
||||||
@end="drag = false"
|
@end="drag = false"
|
||||||
ghost-class="ghost"
|
ghost-class="ghost"
|
||||||
|
v-if="new_supermarket.editmode"
|
||||||
v-bind="{ animation: 200 }"
|
v-bind="{ animation: 200 }"
|
||||||
>
|
>
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<b-card class="m-0 p-0 font-weight-bold no-body list-group-item" style="cursor: move"
|
<b-card no-body v-hover class="mt-1 list-group-item p-2" style="cursor: move" v-for="c in notSupermarketCategory" v-bind:key="c.id" :border-variant="'danger'">
|
||||||
v-for="c in notSupermarketCategory" v-bind:key="c.id" :border-variant="'danger'">
|
<b-card-header class="p-2 border-0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2" v-if="new_supermarket.editmode">
|
||||||
|
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||||
|
</div>
|
||||||
|
<div :class="new_supermarket.editmode ? 'col-10' : 'col-12'">
|
||||||
|
<h5 class="mt-1 mb-1">
|
||||||
{{ categoryName(c) }}
|
{{ categoryName(c) }}
|
||||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
||||||
<i class="btn fas fa-trash fa-lg px-2 text-muted"/>
|
<i class="btn fas fa-trash fa-lg px-2 text-primary" />
|
||||||
</b-button>
|
</b-button>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-card-header>
|
||||||
</b-card>
|
</b-card>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</draggable>
|
</draggable>
|
||||||
@ -343,8 +391,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
|
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="checkbox" class="form-control-sm" v-model="settings.mealplan_autoadd_shopping"
|
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.mealplan_autoadd_shopping" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -356,8 +403,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
|
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="checkbox" class="form-control-sm" v-model="settings.mealplan_autoexclude_onhand"
|
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.mealplan_autoexclude_onhand" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -370,8 +416,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("mealplan_autoinclude_related") }}</div>
|
<div class="col col-md-6">{{ $t("mealplan_autoinclude_related") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="checkbox" class="form-control-sm" v-model="settings.mealplan_autoinclude_related"
|
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.mealplan_autoinclude_related" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -406,8 +451,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("shopping_auto_sync") }}</div>
|
<div class="col col-md-6">{{ $t("shopping_auto_sync") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="number" class="form-control-sm" v-model="settings.shopping_auto_sync"
|
<input type="number" class="form-control" v-model="settings.shopping_auto_sync" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -420,8 +464,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("shopping_add_onhand") }}</div>
|
<div class="col col-md-6">{{ $t("shopping_add_onhand") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="checkbox" class="form-control-sm" v-model="settings.shopping_add_onhand"
|
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.shopping_add_onhand" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -434,11 +477,9 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
|
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="number" class="form-control-sm" v-model="settings.shopping_recent_days"
|
<input type="number" class="form-control" v-model="settings.shopping_recent_days" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<em class="small text-muted">
|
<em class="small text-muted">
|
||||||
@ -449,8 +490,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
|
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="checkbox" class="form-control-sm" v-model="settings.filter_to_supermarket"
|
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.filter_to_supermarket" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -463,8 +503,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("default_delay") }}</div>
|
<div class="col col-md-6">{{ $t("default_delay") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input type="number" class="form-control-sm" min="1" v-model="settings.default_delay"
|
<input type="number" class="form-control" min="1" v-model="settings.default_delay" @change="saveSettings" />
|
||||||
@change="saveSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -477,7 +516,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("csv_delim_label") }}</div>
|
<div class="col col-md-6">{{ $t("csv_delim_label") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input class="form-control-sm" v-model="settings.csv_delim" @change="saveSettings"/>
|
<input class="form-control" v-model="settings.csv_delim" @change="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -490,7 +529,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("csv_prefix_label") }}</div>
|
<div class="col col-md-6">{{ $t("csv_prefix_label") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
<input class="form-control-sm" v-model="settings.csv_prefix" @change="saveSettings"/>
|
<input class="form-control" v-model="settings.csv_prefix" @change="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row sm mb-3">
|
<div class="row sm mb-3">
|
||||||
@ -500,6 +539,19 @@
|
|||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">{{ $t("left_handed") }}</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.left_handed" @change="saveSettings" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row sm mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<em class="small text-muted">
|
||||||
|
{{ $t("left_handed_help") }}
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -511,81 +563,100 @@
|
|||||||
<b-form-select v-model="group_by" :options="group_by_choices" size="sm"></b-form-select>
|
<b-form-select v-model="group_by" :options="group_by_choices" size="sm"></b-form-select>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
|
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
|
||||||
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id"
|
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select>
|
||||||
size="sm"></b-form-select>
|
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<!-- TODO: shade filters red when they are actually filtering content -->
|
<!-- TODO: shade filters red when they are actually filtering content -->
|
||||||
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1">
|
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1">
|
||||||
<b-form-checkbox v-model="show_delay"></b-form-checkbox>
|
<b-form-checkbox v-model="show_delay"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group v-bind:label="$t('ShowUncategorizedFood')" label-for="popover-input-4" content-cols="1"
|
<b-form-group v-bind:label="$t('ShowUncategorizedFood')" label-for="popover-input-4" content-cols="1" class="mb-1" v-if="!selected_supermarket">
|
||||||
class="mb-1" v-if="!selected_supermarket">
|
|
||||||
<b-form-checkbox v-model="show_undefined_categories"></b-form-checkbox>
|
<b-form-checkbox v-model="show_undefined_categories"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
<b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-5" content-cols="1"
|
<b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-5" content-cols="1" class="mb-1" v-if="selected_supermarket">
|
||||||
class="mb-1" v-if="selected_supermarket">
|
|
||||||
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
|
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top: 1vh; min-width: 300px">
|
<div class="row" style="margin-top: 1vh; min-width: 300px">
|
||||||
<div class="col-12" style="text-align: right">
|
<div class="col-12" style="text-align: right">
|
||||||
<b-button size="sm" variant="primary" class="mx-1" @click="resetFilters">{{ $t("Reset") }}</b-button>
|
<b-button size="sm" variant="primary" class="mx-1" @click="resetFilters">{{ $t("Reset") }}</b-button>
|
||||||
<b-button size="sm" variant="secondary" class="mr-3" @click="$root.$emit('bv::hide::popover')">{{
|
<b-button size="sm" variant="secondary" class="mr-3" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
|
||||||
$t("Close")
|
|
||||||
}}
|
|
||||||
</b-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-popover>
|
</b-popover>
|
||||||
<ContextMenu ref="menu">
|
<ContextMenu ref="menu">
|
||||||
<template #menu="{ contextData }">
|
<template #menu="{ contextData }">
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
<b-row class="d-flex align-items-center mr-0">
|
<b-input-group>
|
||||||
<b-col cols="6">
|
<template #prepend>
|
||||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-cubes"></i> {{ $t("MoveCategory") }}</a>
|
<span class="dropdown-item p-2 text-decoration-none" style="user-select: none !important"><i class="fas fa-cubes"></i> {{ $t("MoveCategory") }}</span>
|
||||||
</b-col>
|
</template>
|
||||||
<b-col cols="6 pl-1">
|
<b-form-select
|
||||||
<b-form-select class="form-control form-control-sm" :options="shopping_categories" text-field="name"
|
class="form-control mt-1 mr-1"
|
||||||
value-field="id" v-model="shopcat"
|
:options="shopping_categories"
|
||||||
@change="moveEntry($event, contextData);$refs.menu.close()"></b-form-select>
|
text-field="name"
|
||||||
</b-col>
|
value-field="id"
|
||||||
</b-row>
|
v-model="shopcat"
|
||||||
|
@change="
|
||||||
|
moveEntry($event, contextData)
|
||||||
|
$refs.menu.close()
|
||||||
|
"
|
||||||
|
></b-form-select>
|
||||||
|
</b-input-group>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
<ContextMenuItem @click="$refs.menu.close();onHand(contextData)">
|
@click="
|
||||||
|
$refs.menu.close()
|
||||||
|
onHand(contextData)
|
||||||
|
"
|
||||||
|
>
|
||||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-clipboard-check"></i> {{ $t("OnHand") }}</a>
|
<a class="dropdown-item p-2" href="#"><i class="fas fa-clipboard-check"></i> {{ $t("OnHand") }}</a>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem @click="$refs.menu.close();delayThis(contextData)">
|
<ContextMenuItem
|
||||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-hourglass"></i> {{
|
@click="
|
||||||
$t("DelayFor", {hours: delay})
|
$refs.menu.close()
|
||||||
}}</a>
|
delayThis(contextData)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a class="dropdown-item p-2" href="#"><i class="fas fa-hourglass"></i> {{ $t("DelayFor", { hours: delay }) }}</a>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem @click="$refs.menu.close();updateChecked({ entries: contextData, checked: true })">
|
<ContextMenuItem
|
||||||
|
@click="
|
||||||
|
$refs.menu.close()
|
||||||
|
updateChecked({ entries: contextData, checked: true })
|
||||||
|
"
|
||||||
|
>
|
||||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-check-square"></i> {{ $t("mark_complete") }}</a>
|
<a class="dropdown-item p-2" href="#"><i class="fas fa-check-square"></i> {{ $t("mark_complete") }}</a>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem @click="$refs.menu.close();deleteThis(contextData)">
|
<ContextMenuItem
|
||||||
|
@click="
|
||||||
|
$refs.menu.close()
|
||||||
|
deleteThis(contextData)
|
||||||
|
"
|
||||||
|
>
|
||||||
<a class="dropdown-item p-2 text-danger" href="#"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
<a class="dropdown-item p-2 text-danger" href="#"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</template>
|
</template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<transition name="slided-fade">
|
<transition name="slided-fade">
|
||||||
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none"
|
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
||||||
style="background: rgba(255, 255, 255, 0.6)"
|
<div class="col-6">
|
||||||
v-if="current_tab === 0">
|
|
||||||
<div class="col-md-3 col-6">
|
|
||||||
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode"
|
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode"
|
||||||
><i class="fas fa-cart-plus"></i>
|
><i class="fas fa-cart-plus"></i>
|
||||||
{{ $t("New Entry") }}
|
{{ $t("New Entry") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-6">
|
<div class="col-6">
|
||||||
<a class="btn btn-block btn-secondary shadow-none"
|
<b-dropdown id="dropdown-dropup" block dropup variant="primary" class="shadow-none">
|
||||||
><i class="fas fa-download"></i>
|
<template #button-content> <i class="fas fa-download"></i> {{ $t("Export") }} </template>
|
||||||
{{ $t("Export") }}
|
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||||
</a>
|
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
||||||
|
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')" icon="fas fa-clipboard-list" />
|
||||||
|
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')" icon="fab fa-markdown" />
|
||||||
|
</b-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)" :modal_id="new_recipe.id" @finish="finishShopping" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -604,6 +675,7 @@ import CopyToClipboard from "@/components/Buttons/CopyToClipboard"
|
|||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import GenericPill from "@/components/GenericPill"
|
import GenericPill from "@/components/GenericPill"
|
||||||
import LookupInput from "@/components/Modals/LookupInput"
|
import LookupInput from "@/components/Modals/LookupInput"
|
||||||
|
import ShoppingModal from "@/components/Modals/ShoppingModal"
|
||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
|
|
||||||
import { ApiMixin, getUserPreference, StandardToasts, makeToast } from "@/utils/utils"
|
import { ApiMixin, getUserPreference, StandardToasts, makeToast } from "@/utils/utils"
|
||||||
@ -626,7 +698,8 @@ export default {
|
|||||||
LookupInput,
|
LookupInput,
|
||||||
DownloadPDF,
|
DownloadPDF,
|
||||||
DownloadCSV,
|
DownloadCSV,
|
||||||
CopyToClipboard
|
CopyToClipboard,
|
||||||
|
ShoppingModal,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@ -656,11 +729,13 @@ export default {
|
|||||||
csv_delim: ",",
|
csv_delim: ",",
|
||||||
csv_prefix: undefined,
|
csv_prefix: undefined,
|
||||||
shopping_add_onhand: true,
|
shopping_add_onhand: true,
|
||||||
|
left_handed: false,
|
||||||
},
|
},
|
||||||
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
||||||
new_category: { entrymode: false, value: undefined },
|
new_category: { entrymode: false, value: undefined },
|
||||||
autosync_id: undefined,
|
autosync_id: undefined,
|
||||||
auto_sync_running: false,
|
auto_sync_running: false, // track to not start a new sync before old one was finished
|
||||||
|
auto_sync_blocked: false, // blocking auto sync while request to check item is still running
|
||||||
show_delay: false,
|
show_delay: false,
|
||||||
drag: false,
|
drag: false,
|
||||||
show_modal: false,
|
show_modal: false,
|
||||||
@ -669,6 +744,10 @@ export default {
|
|||||||
entrymode: false,
|
entrymode: false,
|
||||||
new_item: { amount: 1, unit: undefined, food: undefined, ingredient: undefined },
|
new_item: { amount: 1, unit: undefined, food: undefined, ingredient: undefined },
|
||||||
online: true,
|
online: true,
|
||||||
|
new_recipe: {
|
||||||
|
id: undefined,
|
||||||
|
},
|
||||||
|
add_recipe_servings: 1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -706,6 +785,9 @@ export default {
|
|||||||
|
|
||||||
var groups = { false: {}, true: {} } // force unchecked to always be first
|
var groups = { false: {}, true: {} } // force unchecked to always be first
|
||||||
if (this.selected_supermarket) {
|
if (this.selected_supermarket) {
|
||||||
|
// TODO: make nulls_first a user setting
|
||||||
|
groups.false[this.$t("Undefined")] = {}
|
||||||
|
groups.true[this.$t("Undefined")] = {}
|
||||||
let super_cats = this.supermarkets
|
let super_cats = this.supermarkets
|
||||||
.filter((x) => x.id === this.selected_supermarket)
|
.filter((x) => x.id === this.selected_supermarket)
|
||||||
.map((x) => x.category_to_supermarket)
|
.map((x) => x.category_to_supermarket)
|
||||||
@ -716,9 +798,6 @@ export default {
|
|||||||
groups["true"][cat] = {}
|
groups["true"][cat] = {}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// TODO: make nulls_first a user setting
|
|
||||||
groups.false[this.$t("Undefined")] = {}
|
|
||||||
groups.true[this.$t("Undefined")] = {}
|
|
||||||
this.shopping_categories.forEach((cat) => {
|
this.shopping_categories.forEach((cat) => {
|
||||||
groups.false[cat.name] = {}
|
groups.false[cat.name] = {}
|
||||||
groups.true[cat.name] = {}
|
groups.true[cat.name] = {}
|
||||||
@ -767,7 +846,8 @@ export default {
|
|||||||
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.selected_supermarket)
|
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.selected_supermarket)
|
||||||
},
|
},
|
||||||
Recipes() {
|
Recipes() {
|
||||||
return [...new Map(this.items.filter((x) => x.list_recipe).map((item) => [item["list_recipe"], item])).values()]
|
// hiding recipes associated with shopping list items that are complete
|
||||||
|
return [...new Map(this.items.filter((x) => x.list_recipe && !x.checked).map((item) => [item["list_recipe"], item])).values()]
|
||||||
},
|
},
|
||||||
supermarketCategory() {
|
supermarketCategory() {
|
||||||
return this.new_supermarket.editmode ? this.new_supermarket.value.category_to_supermarket : this.shopping_categories
|
return this.new_supermarket.editmode ? this.new_supermarket.value.category_to_supermarket : this.shopping_categories
|
||||||
@ -791,7 +871,13 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
selected_supermarket(newVal, oldVal) {
|
selected_supermarket(newVal, oldVal) {
|
||||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||||
localStorage.setItem('shopping_v2_selected_supermarket', JSON.stringify(this.selected_supermarket))
|
localStorage.setItem("shopping_v2_selected_supermarket", JSON.stringify(this.selected_supermarket))
|
||||||
|
},
|
||||||
|
new_recipe: {
|
||||||
|
handler() {
|
||||||
|
this.add_recipe_servings = this.new_recipe.servings
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
},
|
},
|
||||||
"settings.filter_to_supermarket": function (newVal, oldVal) {
|
"settings.filter_to_supermarket": function (newVal, oldVal) {
|
||||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||||
@ -836,7 +922,7 @@ export default {
|
|||||||
this.$nextTick(function () {
|
this.$nextTick(function () {
|
||||||
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||||
this.entry_mode_simple = this.$cookies.get(SETTINGS_COOKIE_NAME)
|
this.entry_mode_simple = this.$cookies.get(SETTINGS_COOKIE_NAME)
|
||||||
this.selected_supermarket = localStorage.getItem('shopping_v2_selected_supermarket') || undefined
|
this.selected_supermarket = localStorage.getItem("shopping_v2_selected_supermarket") || undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -844,14 +930,21 @@ export default {
|
|||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
addItem: function () {
|
addItem: function () {
|
||||||
if (this.entry_mode_simple) {
|
if (this.entry_mode_simple) {
|
||||||
|
if (this.new_item.ingredient !== "" && this.new_item.ingredient !== undefined) {
|
||||||
this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => {
|
this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => {
|
||||||
|
let unit = null
|
||||||
|
if (result.data.unit !== "") {
|
||||||
|
unit = { name: result.data.unit }
|
||||||
|
}
|
||||||
|
|
||||||
this.new_item = {
|
this.new_item = {
|
||||||
amount: result.data.amount,
|
amount: result.data.amount,
|
||||||
unit: {name: result.data.unit},
|
unit: unit,
|
||||||
food: { name: result.data.food },
|
food: { name: result.data.food },
|
||||||
}
|
}
|
||||||
this.addEntry()
|
this.addEntry()
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.addEntry()
|
this.addEntry()
|
||||||
}
|
}
|
||||||
@ -1008,8 +1101,10 @@ export default {
|
|||||||
}
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
} else {
|
} else {
|
||||||
|
if (!this.auto_sync_blocked) {
|
||||||
this.mergeShoppingList(results.data)
|
this.mergeShoppingList(results.data)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@ -1037,6 +1132,15 @@ export default {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.auto_sync_running = false
|
this.auto_sync_running = false
|
||||||
|
let new_entries = data.map((x) => x.id).filter((y) => !this.items.map((z) => z.id).includes(y))
|
||||||
|
if (new_entries.length > 0) {
|
||||||
|
let api = new ApiApiFactory()
|
||||||
|
new_entries.forEach((new_id) => {
|
||||||
|
api.retrieveShoppingListEntry(new_id).then((result) => {
|
||||||
|
this.items.push(result.data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
moveEntry: function (e, item) {
|
moveEntry: function (e, item) {
|
||||||
if (!e) {
|
if (!e) {
|
||||||
@ -1073,8 +1177,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.then((entries) => {
|
.then((entries) => {
|
||||||
entries.forEach((x) => {
|
entries.forEach((x) => {
|
||||||
api.destroyShoppingListEntry(x).then((result) => {
|
api.destroyShoppingListEntry(x).then((result) => {})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -1132,6 +1235,7 @@ export default {
|
|||||||
},
|
},
|
||||||
updateChecked: function (update) {
|
updateChecked: function (update) {
|
||||||
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
|
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
|
||||||
|
this.auto_sync_blocked = true
|
||||||
let promises = []
|
let promises = []
|
||||||
update.entries.forEach((x) => {
|
update.entries.forEach((x) => {
|
||||||
const id = x?.id ?? x
|
const id = x?.id ?? x
|
||||||
@ -1146,7 +1250,12 @@ export default {
|
|||||||
Vue.set(item, "completed_at", completed_at)
|
Vue.set(item, "completed_at", completed_at)
|
||||||
})
|
})
|
||||||
|
|
||||||
Promise.all(promises).catch((err) => {
|
Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
this.auto_sync_blocked = false
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.auto_sync_blocked = false
|
||||||
console.log(err, err.response)
|
console.log(err, err.response)
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||||
})
|
})
|
||||||
@ -1154,7 +1263,7 @@ export default {
|
|||||||
updateFood: function (food, field) {
|
updateFood: function (food, field) {
|
||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
if (field) {
|
if (field) {
|
||||||
// assume if field is changing it should no longer be inheritted
|
// assume if field is changing it should no longer be inherited
|
||||||
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
|
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1288,6 +1397,24 @@ export default {
|
|||||||
window.removeEventListener("online", this.updateOnlineStatus)
|
window.removeEventListener("online", this.updateOnlineStatus)
|
||||||
window.removeEventListener("offline", this.updateOnlineStatus)
|
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||||
},
|
},
|
||||||
|
addRecipeToShopping() {
|
||||||
|
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
||||||
|
},
|
||||||
|
finishShopping() {
|
||||||
|
this.getShoppingList()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
hover: {
|
||||||
|
inserted: function (el) {
|
||||||
|
el.addEventListener("mouseenter", () => {
|
||||||
|
el.classList.add("shadow")
|
||||||
|
})
|
||||||
|
el.addEventListener("mouseleave", () => {
|
||||||
|
el.classList.remove("shadow")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -1322,28 +1449,34 @@ export default {
|
|||||||
background: #c8ebfb;
|
background: #c8ebfb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-fade-enter-active, .slider-fade-leave-active {
|
.slide-fade-enter-active,
|
||||||
transition: all 0.3s ease;
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider-fade-enter, .slider-fade-leave-to
|
.slide-fade-enter, .slide-fade-leave-to
|
||||||
/* .slider-fade-leave-active below version 2.1.8 */
|
/* .slider-fade-leave-active below version 2.1.8 */ {
|
||||||
{
|
|
||||||
transform: translateX(10px);
|
transform: translateX(10px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slided-fade-enter-active {
|
.form-control-append {
|
||||||
transition: all 0.3s ease;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slided-fade-leave-active {
|
@media (max-width: 768px) {
|
||||||
transition: all 0.1s cubic-bezier(1, 0.5, 0.8, 1);
|
#shoppinglist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 65vh;
|
||||||
|
padding-right: 8px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slided-fade-enter,
|
.settings-checkbox {
|
||||||
.slided-fade-leave-to {
|
font-size: 0.3rem;
|
||||||
transform: translateY(10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -59,7 +59,7 @@ export default {
|
|||||||
addShopping() {
|
addShopping() {
|
||||||
if (this.shopping) {
|
if (this.shopping) {
|
||||||
return
|
return
|
||||||
} // if item already in shopping list, excution handled after confirmation
|
} // if item already in shopping list, execution handled after confirmation
|
||||||
let params = {
|
let params = {
|
||||||
id: this.item.id,
|
id: this.item.id,
|
||||||
amount: 1,
|
amount: 1,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="cursor:pointer">
|
||||||
<a v-if="!button" class="dropdown-item" @click="clipboard"><i :class="icon"></i> {{ label }}</a>
|
<a v-if="!button" class="dropdown-item" @click="clipboard"><i :class="icon"></i> {{ label }}</a>
|
||||||
<b-button v-if="button" @click="clipboard">{{ label }}</b-button>
|
<b-button v-if="button" @click="clipboard">{{ label }}</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="cursor:pointer">
|
||||||
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
|
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
|
||||||
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
|
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="cursor:pointer">
|
||||||
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
|
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
|
||||||
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
|
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -116,7 +116,7 @@ export default {
|
|||||||
},
|
},
|
||||||
addNew(e) {
|
addNew(e) {
|
||||||
this.$emit("new", e)
|
this.$emit("new", e)
|
||||||
// could refactor as Promise - seems unecessary
|
// could refactor as Promise - seems unnecessary
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.search("")
|
this.search("")
|
||||||
}, 750)
|
}, 750)
|
||||||
|
@ -7,8 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6 text-right" v-if="header">
|
<div class="col col-md-6 text-right" v-if="header">
|
||||||
<h4>
|
<h4>
|
||||||
<i v-if="show_shopping && ShoppingRecipes.length > 0" class="fas fa-trash text-danger px-2"
|
<i v-if="show_shopping && ShoppingRecipes.length > 0" class="fas fa-trash text-danger px-2" @click="saveShopping(true)"></i>
|
||||||
@click="saveShopping(true)"></i>
|
|
||||||
<i v-if="show_shopping" class="fas fa-save text-success px-2" @click="saveShopping()"></i>
|
<i v-if="show_shopping" class="fas fa-save text-success px-2" @click="saveShopping()"></i>
|
||||||
<i class="fas fa-shopping-cart px-2" @click="getShopping()"></i>
|
<i class="fas fa-shopping-cart px-2" @click="getShopping()"></i>
|
||||||
</h4>
|
</h4>
|
||||||
@ -16,8 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row text-right" v-if="ShoppingRecipes.length > 1">
|
<div class="row text-right" v-if="ShoppingRecipes.length > 1">
|
||||||
<div class="col col-md-6 offset-md-6 text-right">
|
<div class="col col-md-6 offset-md-6 text-right">
|
||||||
<b-form-select v-model="selected_shoppingrecipe" :options="ShoppingRecipes"
|
<b-form-select v-model="selected_shoppingrecipe" :options="ShoppingRecipes" size="sm"></b-form-select>
|
||||||
size="sm"></b-form-select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br v-if="header" />
|
<br v-if="header" />
|
||||||
@ -26,7 +24,7 @@
|
|||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||||
<template v-for="s in steps">
|
<template v-for="s in steps">
|
||||||
<tr v-bind:key="s.id" v-if="s.show_as_header && s.name !== ''">
|
<tr v-bind:key="s.id" v-if="s.show_as_header && s.name !== '' && !add_shopping_mode">
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<b>{{ s.name }}</b>
|
<b>{{ s.name }}</b>
|
||||||
</td>
|
</td>
|
||||||
@ -99,7 +97,7 @@ export default {
|
|||||||
value: x?.list_recipe,
|
value: x?.list_recipe,
|
||||||
text: x?.recipe_mealplan?.name,
|
text: x?.recipe_mealplan?.name,
|
||||||
recipe: x?.recipe_mealplan?.recipe ?? 0,
|
recipe: x?.recipe_mealplan?.recipe ?? 0,
|
||||||
servings: x?.recipe_mealplan?.servings
|
servings: x?.recipe_mealplan?.servings,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((x) => x?.recipe == this.recipe)
|
.filter((x) => x?.recipe == this.recipe)
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="recipe.keywords.length > 0">
|
<div v-if="recipe.keywords.length > 0">
|
||||||
<span :key="k.id" v-for="k in recipe.keywords" class="pl-1">
|
<span :key="k.id" v-for="k in recipe.keywords" class="pl-1">
|
||||||
<b-badge pill variant="light" class="font-weight-normal">{{k.label}}</b-badge>
|
<a :href="`${resolveDjangoUrl('view_search')}?keyword=${k.id}`"><b-badge pill variant="light"
|
||||||
|
class="font-weight-normal">{{ k.label }}</b-badge></a>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import {ResolveUrlMixin} from "@/utils/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'KeywordsComponent',
|
name: 'KeywordsComponent',
|
||||||
|
mixins: [ResolveUrlMixin],
|
||||||
props: {
|
props: {
|
||||||
recipe: Object,
|
recipe: Object,
|
||||||
},
|
},
|
||||||
|
@ -77,7 +77,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.id = Math.random()
|
this.id = Math.random()
|
||||||
this.$root.$on("change", this.storeValue) // boostrap modal placed at document so have to listen at root of component
|
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<b-modal :id="`shopping_${this.modal_id}`" hide-footer @show="loadRecipe">
|
<b-modal :id="`shopping_${this.modal_id}`" hide-footer @show="loadRecipe">
|
||||||
<template v-slot:modal-title
|
<template v-slot:modal-title
|
||||||
><h4>{{ $t("Add_Servings_to_Shopping", { servings: servings }) }}</h4></template
|
><h4>{{ $t("Add_Servings_to_Shopping", { servings: recipe_servings }) }}</h4></template
|
||||||
>
|
>
|
||||||
<loading-spinner v-if="loading"></loading-spinner>
|
<loading-spinner v-if="loading"></loading-spinner>
|
||||||
<div class="accordion" role="tablist" v-if="!loading">
|
<div class="accordion" role="tablist" v-if="!loading">
|
||||||
@ -15,7 +15,7 @@
|
|||||||
:steps="steps"
|
:steps="steps"
|
||||||
:recipe="recipe.id"
|
:recipe="recipe.id"
|
||||||
:ingredient_factor="ingredient_factor"
|
:ingredient_factor="ingredient_factor"
|
||||||
:servings="servings"
|
:servings="recipe_servings"
|
||||||
:show_shopping="true"
|
:show_shopping="true"
|
||||||
:add_shopping_mode="true"
|
:add_shopping_mode="true"
|
||||||
:header="false"
|
:header="false"
|
||||||
@ -33,7 +33,7 @@
|
|||||||
:steps="r.steps"
|
:steps="r.steps"
|
||||||
:recipe="r.recipe.id"
|
:recipe="r.recipe.id"
|
||||||
:ingredient_factor="ingredient_factor"
|
:ingredient_factor="ingredient_factor"
|
||||||
:servings="servings"
|
:servings="recipe_servings"
|
||||||
:show_shopping="true"
|
:show_shopping="true"
|
||||||
:add_shopping_mode="true"
|
:add_shopping_mode="true"
|
||||||
:header="false"
|
:header="false"
|
||||||
@ -45,12 +45,19 @@
|
|||||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||||
</b-card>
|
</b-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3 mb-3">
|
|
||||||
<div class="col-12 text-right">
|
<b-input-group class="my-3">
|
||||||
<b-button class="mx-2" variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
|
<b-input-group-prepend is-text>
|
||||||
<b-button class="mx-2" variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
|
{{ $t("Servings") }}
|
||||||
</div>
|
</b-input-group-prepend>
|
||||||
</div>
|
|
||||||
|
<b-form-spinbutton min="1" v-model="recipe_servings" inline style="height: 3em"></b-form-spinbutton>
|
||||||
|
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
|
||||||
|
<b-button variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -71,25 +78,37 @@ export default {
|
|||||||
mixins: [],
|
mixins: [],
|
||||||
props: {
|
props: {
|
||||||
recipe: { required: true, type: Object },
|
recipe: { required: true, type: Object },
|
||||||
servings: { type: Number },
|
servings: { type: Number, default: undefined },
|
||||||
modal_id: { required: true, type: Number },
|
modal_id: { required: true, type: Number },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
steps: [],
|
steps: [],
|
||||||
recipe_servings: 0,
|
recipe_servings: undefined,
|
||||||
add_shopping: [],
|
add_shopping: [],
|
||||||
related_recipes: [],
|
related_recipes: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {
|
||||||
|
this.recipe_servings = this.servings
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ingredient_factor: function () {
|
ingredient_factor: function () {
|
||||||
return this.servings / this.recipe.servings || this.recipe_servings
|
return this.recipe_servings / this.recipe.servings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
recipe: {
|
||||||
|
handler() {
|
||||||
|
this.loadRecipe()
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
servings: function (newVal) {
|
||||||
|
this.recipe_servings = parseInt(newVal)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {},
|
|
||||||
methods: {
|
methods: {
|
||||||
loadRecipe: function () {
|
loadRecipe: function () {
|
||||||
this.add_shopping = []
|
this.add_shopping = []
|
||||||
@ -109,7 +128,9 @@ export default {
|
|||||||
.filter((x) => !x?.food?.food_onhand)
|
.filter((x) => !x?.food?.food_onhand)
|
||||||
.map((x) => x.id),
|
.map((x) => x.id),
|
||||||
]
|
]
|
||||||
|
if (!this.recipe_servings) {
|
||||||
this.recipe_servings = result.data?.servings
|
this.recipe_servings = result.data?.servings
|
||||||
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -159,19 +180,27 @@ export default {
|
|||||||
let shopping_recipe = {
|
let shopping_recipe = {
|
||||||
id: this.recipe.id,
|
id: this.recipe.id,
|
||||||
ingredients: this.add_shopping,
|
ingredients: this.add_shopping,
|
||||||
servings: this.servings,
|
servings: this.recipe_servings,
|
||||||
}
|
}
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
apiClient
|
apiClient
|
||||||
.shoppingRecipe(this.recipe.id, shopping_recipe)
|
.shoppingRecipe(this.recipe.id, shopping_recipe)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
|
this.$emit("finish")
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$bvModal.hide(`shopping_${this.modal_id}`)
|
this.$bvModal.hide(`shopping_${this.modal_id}`)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.b-form-spinbutton.form-control {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="shopping_line_item">
|
<div id="shopping_line_item">
|
||||||
<b-container fluid class="pr-0 pl-1 pl-md-3">
|
|
||||||
<!-- summary rows -->
|
|
||||||
<b-row align-h="start">
|
<b-row align-h="start">
|
||||||
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
|
||||||
|
v-if="settings.left_handed">
|
||||||
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked"
|
||||||
|
@change="updateChecked"
|
||||||
|
:key="entries[0].id"/>
|
||||||
|
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
||||||
|
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
|
||||||
|
</div>
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
<b-col cols="1" class="align-items-center d-flex">
|
<b-col cols="1" class="align-items-center d-flex">
|
||||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
||||||
|
@click.stop="$emit('open-context-menu', $event, entries)">
|
||||||
<button
|
<button
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
|
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
||||||
@click.stop="$emit('open-context-menu', $event, entries)">
|
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret">
|
||||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -21,23 +30,25 @@
|
|||||||
:key="entries[0].id"/>
|
:key="entries[0].id"/>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="8" md="9">
|
<b-col cols="8" md="9">
|
||||||
<b-row class="d-flex h-100" @click.stop="$emit('open-context-menu', $event, entries)">
|
<b-row class="d-flex h-100">
|
||||||
<b-col cols="6" md="6" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
|
<b-col cols="5" md="3" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
|
||||||
<div><strong>{{ Object.entries(formatAmount)[0][1] }}</strong>  
|
<strong class="mr-1">{{ Object.entries(formatAmount)[0][1] }}</strong> {{
|
||||||
{{ Object.entries(formatAmount)[0][0] }}
|
Object.entries(formatAmount)[0][0]
|
||||||
</div>
|
}}
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="6" md="6" class="d-flex flex-column" v-if="Object.entries(formatAmount).length != 1">
|
<b-col cols="5" md="3" class="d-flex flex-column" v-if="Object.entries(formatAmount).length != 1">
|
||||||
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }}  
|
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }}  
|
||||||
{{ x[0] }}
|
{{ x[0] }}
|
||||||
</div>
|
</div>
|
||||||
</b-col>
|
</b-col>
|
||||||
|
|
||||||
<b-col cols="6" md="3" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
||||||
{{ formatFood }}
|
{{ formatFood }}
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="3" data-html2canvas-ignore="true" class="align-items-center d-none d-md-flex">
|
<b-col cols="3" data-html2canvas-ignore="true"
|
||||||
<b-button size="sm" @click="showDetails = !showDetails" class="p-0 mr-0 mr-md-2 p-md-2" variant="link">
|
class="align-items-center d-none d-md-flex justify-content-end">
|
||||||
|
<b-button size="sm" @click="showDetails = !showDetails" class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none"
|
||||||
|
variant="link">
|
||||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate"
|
<div class="text-nowrap"><i class="fa fa-chevron-right rotate"
|
||||||
:class="showDetails ? 'rotated' : ''"></i> <span
|
:class="showDetails ? 'rotated' : ''"></i> <span
|
||||||
class="d-none d-md-inline-block">{{ $t('Details') }}</span>
|
class="d-none d-md-inline-block">{{ $t('Details') }}</span>
|
||||||
@ -46,7 +57,8 @@
|
|||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none">
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
|
||||||
|
v-if="!settings.left_handed">
|
||||||
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
|
||||||
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
|
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
|
||||||
</div>
|
</div>
|
||||||
@ -61,10 +73,8 @@
|
|||||||
<div class="small text-muted text-truncate">{{ formatHint }}</div>
|
<div class="small text-muted text-truncate">{{ formatHint }}</div>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
|
||||||
<!-- detail rows -->
|
<!-- detail rows -->
|
||||||
<div class="card no-body mb-1 pt-2 align-content-center ml-2" v-if="showDetails">
|
<div class="card no-body mb-1 pt-2 align-content-center shadow-sm" v-if="showDetails">
|
||||||
<b-container fluid>
|
|
||||||
<div v-for="(e, x) in entries" :key="e.id">
|
<div v-for="(e, x) in entries" :key="e.id">
|
||||||
<b-row class="small justify-content-around">
|
<b-row class="small justify-content-around">
|
||||||
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
|
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
|
||||||
@ -72,7 +82,7 @@
|
|||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-link btn-sm m-0 p-0"
|
class="btn btn-link btn-sm m-0 p-0 pl-2"
|
||||||
style="text-overflow: ellipsis"
|
style="text-overflow: ellipsis"
|
||||||
@click.stop="openRecipeCard($event, e)"
|
@click.stop="openRecipeCard($event, e)"
|
||||||
@mouseover="openRecipeCard($event, e)">
|
@mouseover="openRecipeCard($event, e)">
|
||||||
@ -80,24 +90,28 @@
|
|||||||
</button>
|
</button>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="auto" md="4" class="text-muted">{{ formatOneMealPlan(e) }}</b-col>
|
<b-col cols="auto" md="4" class="text-muted">{{ formatOneMealPlan(e) }}</b-col>
|
||||||
<b-col cols="auto" md="4" class="text-muted text-right overflow-hidden text-nowrap">
|
<b-col cols="auto" md="4" class="text-muted text-right overflow-hidden text-nowrap pr-4">
|
||||||
{{ formatOneCreatedBy(e) }}
|
{{ formatOneCreatedBy(e) }}
|
||||||
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
|
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<b-row align-h="start">
|
<b-row align-h="start">
|
||||||
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
|
||||||
|
v-if="settings.left_handed">
|
||||||
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
||||||
|
:checked="formatChecked"
|
||||||
|
@change="updateChecked"
|
||||||
|
:key="entries[0].id"/>
|
||||||
|
</b-col>
|
||||||
<b-col cols="1" class="align-items-center d-flex">
|
<b-col cols="1" class="align-items-center d-flex">
|
||||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
|
||||||
|
@click.stop="$emit('open-context-menu', $event, e)">
|
||||||
<button
|
<button
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
|
:class="settings.left_handed ? 'dropdown-spacing' : ''"
|
||||||
@click.stop="$emit('open-context-menu', $event, e)">
|
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret">
|
||||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -108,14 +122,11 @@
|
|||||||
:key="entries[0].id"/>
|
:key="entries[0].id"/>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="8" md="9">
|
<b-col cols="8" md="9">
|
||||||
<b-row class="d-flex justify-content-around">
|
<b-row class="d-flex align-items-center h-100">
|
||||||
<b-col cols="6" md="6" class="d-flex align-items-center">
|
<b-col cols="5" md="3" class="d-flex align-items-center">
|
||||||
<div>{{ formatOneAmount(e) }}  
|
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}
|
||||||
{{ formatOneUnit(e) }}
|
|
||||||
</div>
|
|
||||||
</b-col>
|
</b-col>
|
||||||
|
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
||||||
<b-col cols="6" md="3" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
|
|
||||||
{{ formatOneFood(e) }}
|
{{ formatOneFood(e) }}
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" class="d-flex d-md-none">
|
<b-col cols="12" class="d-flex d-md-none">
|
||||||
@ -123,7 +134,8 @@
|
|||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none">
|
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
|
||||||
|
v-if="!settings.left_handed">
|
||||||
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
|
||||||
:checked="formatChecked"
|
:checked="formatChecked"
|
||||||
@change="updateChecked"
|
@change="updateChecked"
|
||||||
@ -133,7 +145,6 @@
|
|||||||
<hr class="w-75" v-if="x !== entries.length -1"/>
|
<hr class="w-75" v-if="x !== entries.length -1"/>
|
||||||
<div class="pb-4" v-if="x === entries.length -1"></div>
|
<div class="pb-4" v-if="x === entries.length -1"></div>
|
||||||
</div>
|
</div>
|
||||||
</b-container>
|
|
||||||
</div>
|
</div>
|
||||||
<hr class="m-1" v-if="!showDetails"/>
|
<hr class="m-1" v-if="!showDetails"/>
|
||||||
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
|
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
|
||||||
@ -177,6 +188,7 @@ export default {
|
|||||||
entries: {
|
entries: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
settings: Object,
|
||||||
groupby: {type: String},
|
groupby: {type: String},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -355,4 +367,11 @@ export default {
|
|||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dropdown-spacing {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -213,7 +213,7 @@
|
|||||||
"IgnoreThis": "{food} nicht automatisch zur Einkaufsliste hinzufügen",
|
"IgnoreThis": "{food} nicht automatisch zur Einkaufsliste hinzufügen",
|
||||||
"shopping_auto_sync": "Automatische Synchronisierung",
|
"shopping_auto_sync": "Automatische Synchronisierung",
|
||||||
"shopping_share_desc": "Benutzer sehen all Einträge, die du zur Einkaufsliste hinzufügst. Sie müssen dich hinzufügen, damit du Ihre Einträge sehen kannst.",
|
"shopping_share_desc": "Benutzer sehen all Einträge, die du zur Einkaufsliste hinzufügst. Sie müssen dich hinzufügen, damit du Ihre Einträge sehen kannst.",
|
||||||
"IgnoredFood": "{food} beim nächsten Einkauf ignorieren",
|
"IgnoredFood": "{food} beim nächsten Einkauf ignorieren.",
|
||||||
"Add_Servings_to_Shopping": "{servings} Portionen zum Einkauf hinzufügen",
|
"Add_Servings_to_Shopping": "{servings} Portionen zum Einkauf hinzufügen",
|
||||||
"Inherit": "Vererben",
|
"Inherit": "Vererben",
|
||||||
"InheritFields": "Feldwerte vererben",
|
"InheritFields": "Feldwerte vererben",
|
||||||
@ -233,8 +233,8 @@
|
|||||||
"AddToShopping": "Zur Einkaufsliste hinzufügen",
|
"AddToShopping": "Zur Einkaufsliste hinzufügen",
|
||||||
"FoodOnHand": "Sie haben {food} vorrätig.",
|
"FoodOnHand": "Sie haben {food} vorrätig.",
|
||||||
"DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste zu entfernen?",
|
"DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste zu entfernen?",
|
||||||
"err_moving_resource": "Ein Fehler trat während des Verschiebens einer Ressource auf!",
|
"err_moving_resource": "Während des Verschiebens einer Resource ist ein Fehler aufgetreten!",
|
||||||
"err_merging_resource": "Beim Zusammenführen einer Relssource trat ein Fehler auf!",
|
"err_merging_resource": "Beim Zusammenführen einer Ressource ist ein Fehler aufgetreten!",
|
||||||
"success_moving_resource": "Ressource wurde erfolgreich verschoben!",
|
"success_moving_resource": "Ressource wurde erfolgreich verschoben!",
|
||||||
"success_merging_resource": "Ressource wurde erfolgreich zusammengeführt!",
|
"success_merging_resource": "Ressource wurde erfolgreich zusammengeführt!",
|
||||||
"Shopping_Categories": "Einkaufskategorien",
|
"Shopping_Categories": "Einkaufskategorien",
|
||||||
|
@ -287,5 +287,7 @@
|
|||||||
"sql_debug": "SQL Debug",
|
"sql_debug": "SQL Debug",
|
||||||
"remember_search": "Remember Search",
|
"remember_search": "Remember Search",
|
||||||
"remember_hours": "Hours to Remember",
|
"remember_hours": "Hours to Remember",
|
||||||
"tree_select": "Use Tree Selection"
|
"tree_select": "Use Tree Selection",
|
||||||
|
"left_handed": "Left-handed mode",
|
||||||
|
"left_handed_help": "Will optimize the UI for use with your left hand."
|
||||||
}
|
}
|
||||||
|
@ -284,5 +284,6 @@
|
|||||||
"QuickEntry": "Szybki wpis",
|
"QuickEntry": "Szybki wpis",
|
||||||
"related_recipes": "Powiązane przepisy",
|
"related_recipes": "Powiązane przepisy",
|
||||||
"today_recipes": "Dzisiejsze przepisy",
|
"today_recipes": "Dzisiejsze przepisy",
|
||||||
"Search Settings": "Ustawienia wyszukiwania"
|
"Search Settings": "Ustawienia wyszukiwania",
|
||||||
|
"Pin": "Pin"
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"all_fields_optional": "所有字段都是可选的,可以留空。",
|
"all_fields_optional": "所有字段都是可选的,可以留空。",
|
||||||
"convert_internal": "转换为内部菜谱",
|
"convert_internal": "转换为内部菜谱",
|
||||||
"show_only_internal": "仅显示内部菜谱",
|
"show_only_internal": "仅显示内部菜谱",
|
||||||
"Log_Recipe_Cooking": "菜谱烹饪纪录",
|
"Log_Recipe_Cooking": "菜谱烹饪记录",
|
||||||
"External_Recipe_Image": "外部菜谱图像",
|
"External_Recipe_Image": "外部菜谱图像",
|
||||||
"Add_to_Shopping": "添加到购物",
|
"Add_to_Shopping": "添加到购物",
|
||||||
"Add_to_Plan": "添加到计划",
|
"Add_to_Plan": "添加到计划",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"Categories": "分类",
|
"Categories": "分类",
|
||||||
"Category": "分类",
|
"Category": "分类",
|
||||||
"Selected": "选定",
|
"Selected": "选定",
|
||||||
"min": "",
|
"min": "分钟",
|
||||||
"Servings": "份量",
|
"Servings": "份量",
|
||||||
"Waiting": "等待",
|
"Waiting": "等待",
|
||||||
"Preparation": "准备",
|
"Preparation": "准备",
|
||||||
@ -74,7 +74,7 @@
|
|||||||
"Print": "打印",
|
"Print": "打印",
|
||||||
"Settings": "设置",
|
"Settings": "设置",
|
||||||
"or": "或",
|
"or": "或",
|
||||||
"and": "与",
|
"and": "和",
|
||||||
"Information": "更多信息",
|
"Information": "更多信息",
|
||||||
"Download": "下载",
|
"Download": "下载",
|
||||||
"Create": "创建",
|
"Create": "创建",
|
||||||
@ -165,5 +165,115 @@
|
|||||||
"Create_New_Shopping Category": "创建新的购物类别",
|
"Create_New_Shopping Category": "创建新的购物类别",
|
||||||
"Automate": "自动化",
|
"Automate": "自动化",
|
||||||
"Empty": "空的",
|
"Empty": "空的",
|
||||||
"SupermarketName": "超市名"
|
"SupermarketName": "超市名",
|
||||||
|
"NotInShopping": "购物清单中没有 {food}。",
|
||||||
|
"Drag_Here_To_Delete": "拖动此处可删除",
|
||||||
|
"DeleteShoppingConfirm": "确定要移除购物清单中所有 {food} 吗?",
|
||||||
|
"ShowDelayed": "显示延迟的项目",
|
||||||
|
"mealplan_autoexclude_onhand": "排除入手的食物",
|
||||||
|
"shopping_share_desc": "用户将看到您添加到购物清单中的所有商品。他们必须添加你才能看到他们清单上的内容。",
|
||||||
|
"shopping_auto_sync_desc": "设置为0将禁用自动同步。当查看购物列表时,该列表每隔一秒更新一次,以同步其他人可能做出的更改。在多人购物时很有用,但会使用移动数据。",
|
||||||
|
"err_merge_self": "无法将项目与自身合并",
|
||||||
|
"CategoryInstruction": "拖动类别可更改出现在购物清单中的订单类别。",
|
||||||
|
"csv_prefix_help": "将清单复制到剪贴板时要添加的前缀。",
|
||||||
|
"remember_search": "记住搜索",
|
||||||
|
"Root": "根",
|
||||||
|
"Instructions": "说明",
|
||||||
|
"Period": "周期",
|
||||||
|
"Plan_Period_To_Show": "显示星期、月或年",
|
||||||
|
"Periods": "周期",
|
||||||
|
"Plan_Show_How_Many_Periods": "要显示多少个周期",
|
||||||
|
"Starting_Day": "一周中的第一天",
|
||||||
|
"Meal_Types": "用餐类型",
|
||||||
|
"Make_header": "显示注意事项",
|
||||||
|
"Color": "颜色",
|
||||||
|
"New_Meal_Type": "新用餐类型",
|
||||||
|
"Pin": "固定",
|
||||||
|
"Planner_Settings": "计划者设置",
|
||||||
|
"Meal_Type": "用餐类型",
|
||||||
|
"Clone": "复制",
|
||||||
|
"Title_or_Recipe_Required": "需要选择标题或菜谱",
|
||||||
|
"Export_As_ICal": "将当前周期导出为 iCal 格式",
|
||||||
|
"Week_Numbers": "周数",
|
||||||
|
"Show_Week_Numbers": "显示周数?",
|
||||||
|
"Coming_Soon": "即将到来",
|
||||||
|
"New_Cookbook": "新烹饪书",
|
||||||
|
"Hide_Keyword": "隐藏关键词",
|
||||||
|
"Export_To_ICal": "导出 .ics",
|
||||||
|
"Added_To_Shopping_List": "添加到购物清单",
|
||||||
|
"Cannot_Add_Notes_To_Shopping": "无法将笔记添加到购物清单",
|
||||||
|
"Shopping_List_Empty": "您的购物列表当前为空,您可以通过用餐计划条目的上下文菜单添加项目(右键单击卡片或左键单击菜单图标)",
|
||||||
|
"Next_Period": "下期",
|
||||||
|
"Current_Period": "本期",
|
||||||
|
"Next_Day": "第二天",
|
||||||
|
"Previous_Period": "上期",
|
||||||
|
"Previous_Day": "前一天",
|
||||||
|
"remember_hours": "需要记住的时间",
|
||||||
|
"tree_select": "使用树形选择",
|
||||||
|
"Make_Ingredient": "显示材料",
|
||||||
|
"Note": "笔记",
|
||||||
|
"Added_on": "添加到",
|
||||||
|
"AddToShopping": "添加到购物清单",
|
||||||
|
"IngredientInShopping": "此材料已在购物清单。",
|
||||||
|
"OnHand": "目前",
|
||||||
|
"FoodOnHand": "你手上有 {food}。",
|
||||||
|
"FoodNotOnHand": "你还没有 {food}。",
|
||||||
|
"Undefined": "未定义的",
|
||||||
|
"Create_Meal_Plan_Entry": "创建用餐计划条目",
|
||||||
|
"Edit_Meal_Plan_Entry": "编辑用餐计划条目",
|
||||||
|
"Title": "标题",
|
||||||
|
"Week": "星期",
|
||||||
|
"Month": "月份",
|
||||||
|
"Year": "年",
|
||||||
|
"Planner": "计划者",
|
||||||
|
"Meal_Type_Required": "用餐类型是必需的",
|
||||||
|
"AddFoodToShopping": "添加 {food} 到购物清单",
|
||||||
|
"RemoveFoodFromShopping": "从购物清单中移除 {food}",
|
||||||
|
"IgnoredFood": "已忽略购买 {food}。",
|
||||||
|
"Add_Servings_to_Shopping": "添加 {servings} 份到购物",
|
||||||
|
"Inherit": "继承",
|
||||||
|
"InheritFields": "继承字段值",
|
||||||
|
"FoodInherit": "食物可继承的字段",
|
||||||
|
"ShowUncategorizedFood": "显示未定义",
|
||||||
|
"GroupBy": "分组",
|
||||||
|
"MoveCategory": "移动到: ",
|
||||||
|
"IgnoreThis": "永不自动添加 {food} 到购物",
|
||||||
|
"DelayFor": "延迟 {hours} 小时",
|
||||||
|
"Warning": "警告",
|
||||||
|
"NoCategory": "未选择分类。",
|
||||||
|
"Completed": "完成",
|
||||||
|
"OfflineAlert": "您处于离线状态,购物清单可能无法同步。",
|
||||||
|
"shopping_share": "分享购物清单",
|
||||||
|
"shopping_auto_sync": "自动同步",
|
||||||
|
"mealplan_autoadd_shopping": "自动添加用餐计划",
|
||||||
|
"mealplan_autoinclude_related": "添加相关的菜谱",
|
||||||
|
"default_delay": "默认延迟时间",
|
||||||
|
"mealplan_autoadd_shopping_desc": "自动将用餐计划配料添加到购物清单中。",
|
||||||
|
"mealplan_autoexclude_onhand_desc": "将用餐计划添加到购物清单时(手动或自动),排除当前手头上的配料。",
|
||||||
|
"mealplan_autoinclude_related_desc": "将用餐计划(手动或自动)添加到购物清单时,包括所有相关菜谱。",
|
||||||
|
"default_delay_desc": "延迟购物清单条目的默认小时数。",
|
||||||
|
"err_move_self": "无法将项目移动到自身",
|
||||||
|
"nothing": "无事可做",
|
||||||
|
"show_sql": "显示 SQL",
|
||||||
|
"filter_to_supermarket_desc": "默认情况下,过滤购物清单只包括所选超市的类别。",
|
||||||
|
"shopping_recent_days_desc": "显示最近几天的购物清单条目。",
|
||||||
|
"shopping_recent_days": "最近几天",
|
||||||
|
"create_shopping_new": "添加到新的购物清单",
|
||||||
|
"download_pdf": "下载 PDF",
|
||||||
|
"download_csv": "下载 CSV",
|
||||||
|
"csv_delim_help": "用于 CSV 导出的分隔符。",
|
||||||
|
"csv_delim_label": "CSV 分隔符",
|
||||||
|
"SuccessClipboard": "购物清单已复制到剪贴板",
|
||||||
|
"copy_to_clipboard": "复制到剪贴板",
|
||||||
|
"csv_prefix_label": "清单前缀",
|
||||||
|
"copy_markdown_table": "复制为 Markdown 表格",
|
||||||
|
"in_shopping": "在购物清单上",
|
||||||
|
"DelayUntil": "推迟到",
|
||||||
|
"mark_complete": "标记完成",
|
||||||
|
"QuickEntry": "快速入口",
|
||||||
|
"shopping_add_onhand_desc": "在核对购物清单时,将食物标记为“入手”。",
|
||||||
|
"shopping_add_onhand": "自动入手",
|
||||||
|
"related_recipes": "相关的菜谱",
|
||||||
|
"today_recipes": "今日菜谱",
|
||||||
|
"sql_debug": "调试 SQL"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user