This commit is contained in:
smilerz 2021-10-26 19:56:44 -05:00
parent 5c9f5e0e1a
commit 7c598720d0
9 changed files with 177 additions and 765 deletions

View File

@ -479,8 +479,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
model = UserPreference
fields = (
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket'
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand', 'default_delay'
)
help_texts = {
@ -491,18 +490,14 @@ class ShoppingPreferenceForm(forms.ModelForm):
),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoexclude_onhand': _('When automatically adding a meal plan to the shopping list, exclude ingredients that are on hand.'),
'mealplan_autoinclude_related': _('When automatically adding a meal plan to the shopping list, include all related recipes.'),
'default_delay': _('Default number of hours to delay a shopping list entry.'),
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
}
labels = {
'shopping_share': _('Share Shopping List'),
'shopping_auto_sync': _('Autosync'),
'mealplan_autoadd_shopping': _('Auto Add Meal Plan'),
'mealplan_autoexclude_onhand': _('Exclude On Hand'),
'mealplan_autoinclude_related': _('Include Related'),
'default_delay': _('Default Delay Hours'),
'filter_to_supermarket': _('Filter to Supermarket'),
}
widgets = {

View File

@ -23,11 +23,6 @@ def shopping_helper(qs, request):
supermarket_categories = SupermarketCategoryRelation.objects.filter(supermarket=supermarket, category=OuterRef('food__supermarket_category'))
qs = qs.annotate(supermarket_order=Coalesce(Subquery(supermarket_categories.values('order')), Value(9999)))
supermarket_order = ['supermarket_order'] + supermarket_order
# if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
# qs = qs.annotate(recipe_notes=ArrayAgg('list_recipe__recipe__steps__ingredients__note', filter=Q(list_recipe__recipe__steps__ingredients__food=F('food_id'))))
# qs = qs.annotate(meal_notes=ArrayAgg('list_recipe__mealplan__note', distinct=True, filter=Q(list_recipe__mealplan__note__isnull=False)))
# else:
# pass # ignore adding notes when running sqlite? or do some ugly contruction?
if checked in ['false', 0, '0']:
qs = qs.filter(checked=False)
elif checked in ['true', 1, '1']:
@ -39,4 +34,4 @@ def shopping_helper(qs, request):
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
supermarket_order = ['checked'] + supermarket_order
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'list_recipe__mealplan', 'list_recipe__recipe')
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')

View File

@ -17,6 +17,7 @@ from django.db import IntegrityError, models
from django.db.models import Index, ProtectedError, Q, Subquery
from django.db.models.fields.related import ManyToManyField
from django.db.models.functions import Substr
from django.db.transaction import atomic
from django.utils import timezone
from django.utils.translation import gettext as _
from django_prometheus.models import ExportModelOperationsMixin
@ -328,6 +329,7 @@ class UserPreference(models.Model, PermissionModelMixin):
mealplan_autoadd_shopping = models.BooleanField(default=False)
mealplan_autoexclude_onhand = models.BooleanField(default=True)
mealplan_autoinclude_related = models.BooleanField(default=True)
default_delay = models.IntegerField(default=4)
created_at = models.DateTimeField(auto_now_add=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
@ -823,7 +825,7 @@ class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), mod
def get_owner(self):
try:
return getattr(self.entries.first(), 'created_by', None) or getattr(self.shoppinglist_set.first(), 'created_by', None)
return self.entries.first().created_by or self.shoppinglist_set.first().created_by
except AttributeError:
return None
@ -839,11 +841,13 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
delay_until = models.DateTimeField(null=True, blank=True)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space')
@ classmethod
@classmethod
@atomic
def list_from_recipe(self, list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None):
"""
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
@ -853,22 +857,51 @@ class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), model
:param servings: Optional: Number of servings to use to scale shoppinglist. If servings = 0 an existing recipe list will be deleted
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
"""
# TODO cascade to associated recipes
try:
r = recipe or mealplan.recipe
except AttributeError:
# TODO cascade to related recipes
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
if not r:
raise ValueError(_("You must supply a recipe or mealplan"))
created_by = created_by or getattr(mealplan, 'created_by', None)
created_by = created_by or getattr(mealplan, 'created_by', None) or getattr(list_recipe, 'created_by', None)
if not created_by:
raise ValueError(_("You must supply a created_by"))
servings = servings or getattr(mealplan, 'servings', 1.0)
if ingredients:
if type(servings) not in [int, float]:
servings = getattr(mealplan, 'servings', 1.0)
shared_users = list(created_by.get_shopping_share())
shared_users.append(created_by)
if list_recipe:
created = False
else:
list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
created = True
if servings == 0 and not created:
list_recipe.delete()
return []
elif ingredients:
ingredients = Ingredient.objects.filter(pk__in=ingredients, space=space)
else:
ingredients = Ingredient.objects.filter(step__recipe=r, space=space)
list_recipe = ShoppingListRecipe.objects.create(recipe=r, mealplan=mealplan, servings=servings)
existing_list = ShoppingListEntry.objects.filter(list_recipe=list_recipe)
# delete shopping list entries not included in ingredients
existing_list.exclude(ingredient__in=ingredients).delete()
# add shopping list entries that did not previously exist
add_ingredients = set(ingredients.values_list('id', flat=True)) - set(existing_list.values_list('ingredient__id', flat=True))
add_ingredients = Ingredient.objects.filter(id__in=add_ingredients, space=space)
# if servings have changed, update the ShoppingListRecipe and existing Entrys
if servings <= 0:
servings = 1
servings_factor = servings / r.servings
if not created and list_recipe.servings != servings:
update_ingredients = set(ingredients.values_list('id', flat=True)) - set(add_ingredients.values_list('id', flat=True))
for sle in ShoppingListEntry.objects.filter(list_recipe=list_recipe, ingredient__id__in=update_ingredients):
sle.amount = sle.ingredient.amount * Decimal(servings_factor)
sle.save()
# add any missing Entrys
shoppinglist = [
ShoppingListEntry(
list_recipe=list_recipe,

View File

@ -162,7 +162,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default'
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay'
)
@ -392,7 +392,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
model = Food
fields = (
'id', 'name', 'description', 'shopping', 'recipe', 'ignore_shopping', 'supermarket_category',
'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'inherit', 'ignore_inherit'
'image', 'parent', 'numchild', 'numrecipe', 'on_hand', 'inherit', 'ignore_inherit',
)
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
@ -634,8 +634,26 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
mealplan_note = serializers.ReadOnlyField(source='mealplan.note')
servings = CustomDecimalField()
def get_note_markdown(self, obj):
return obj.mealplan and markdown(obj.mealplan.note)
def get_name(self, obj):
if not isinstance(value := obj.servings, Decimal):
value = Decimal(value)
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
def update(self, instance, validated_data):
if 'servings' in validated_data:
ShoppingListEntry.list_from_recipe(
list_recipe=instance,
servings=validated_data['servings'],
created_by=self.context['request'].user,
space=self.context['request'].space
)
return super().update(instance, validated_data)
class Meta:
model = ShoppingListRecipe
@ -649,7 +667,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserNameSerializer()
created_by = UserNameSerializer(read_only=True)
completed_at = serializers.DateTimeField(allow_null=True)
def get_fields(self, *args, **kwargs):
fields = super().get_fields(*args, **kwargs)
@ -684,7 +703,8 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
data['completed_at'] = None
else:
# otherwise don't write anything
del data['completed_at']
if 'completed_at' in data:
del data['completed_at']
############################################################
# temporary while old and new shopping lists are both in use
@ -711,9 +731,9 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
model = ShoppingListEntry
fields = (
'id', 'list_recipe', 'food', 'unit', 'ingredient', 'ingredient_note', 'amount', 'order', 'checked', 'recipe_mealplan',
'created_by', 'created_at', 'completed_at'
'created_by', 'created_at', 'completed_at', 'delay_until'
)
read_only_fields = ('id', 'list_recipe', 'created_by', 'created_at',)
read_only_fields = ('id', 'created_by', 'created_at',)
# TODO deprecate

View File

@ -385,7 +385,6 @@ def user_settings(request):
up.mealplan_autoexclude_onhand = shopping_form.cleaned_data['mealplan_autoexclude_onhand']
up.mealplan_autoinclude_related = shopping_form.cleaned_data['mealplan_autoinclude_related']
up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync']
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
up.default_delay = shopping_form.cleaned_data['default_delay']
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL

View File

@ -1,460 +0,0 @@
<template>
<!-- add alert at top if offline -->
<!-- get autosync time from preferences and put fetching checked items on timer -->
<!-- allow reordering or items -->
<div id="app" style="margin-bottom: 4vh">
<div class="row float-top">
<div class="offset-md-10 col-md-2 no-gutter text-right">
<b-button variant="link" class="px-0">
<i class="btn fas fa-plus-circle text-muted fa-lg" @click="startAction({ action: 'new' })" />
</b-button>
<b-button variant="link" id="id_filters_button" class="px-0">
<i class="btn fas fa-filter text-decoration-none text-muted fa-lg" />
</b-button>
</div>
</div>
<b-tabs content-class="mt-3">
<b-tab :title="$t('ShoppingList')" active>
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("ShoppingList") }} </template>
<div class="row">
<div class="col col-md-12">
<!-- TODO add spinner -->
<div role="tablist" v-if="items && items.length > 0">
<!-- WARNING: all data in the table should be considered read-only, don't change any data through table bindings -->
<div v-for="(done, x) in Sections" :key="x">
<div v-if="x == 'true'">
<hr />
<hr />
<h4>{{ $t("Completed") }}</h4>
</div>
<div v-for="(s, i) in done" :key="i">
<h5>
<b-button
class="btn btn-lg text-decoration-none text-dark px-1 py-0 border-0"
variant="link"
data-toggle="collapse"
:href="'#section-' + sectionID(x, i)"
:aria-expanded="'true' ? x == 'false' : 'true'"
>
<i class="fa fa-chevron-right rotate" />
{{ i }}
</b-button>
</h5>
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
<!-- passing an array of values to the table grouped by Food -->
<div v-for="(entries, x) in Object.entries(s)" :key="x">
<ShoppingLineItem
:entries="entries[1]"
:groupby="group_by"
@open-context-menu="openContextMenu"
@toggle-checkbox="toggleChecked"
></ShoppingLineItem>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</b-tab>
<b-tab :title="$t('Settings')">
These are the settings <br />-sort supermarket categories<br />
-add supermarket categories<br />
- add supermarkets autosync time<br />
autosync on/off<br />
always restrict supermarket to categories?<br />
when restricted or filterd - give visual indication<br />
how long to defer shopping - default tomorrow
</b-tab>
</b-tabs>
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
<div>
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-3">
<b-form-select v-model="group_by" :options="group_by_choices" size="sm"></b-form-select>
</b-form-group>
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-3">
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select>
</b-form-group>
<b-form-group v-bind:label="$t('ShowUncategorizedFood')" label-for="popover-input-2" label-cols="6" class="mb-3 text-nowrap">
<b-form-checkbox v-model="show_undefined_categories"></b-form-checkbox>
</b-form-group>
<b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-3" label-cols="6" class="mb-3">
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
</b-form-group>
</div>
<div class="row" style="margin-top: 1vh;min-width:300px">
<div class="col-12" style="text-align: right;">
<b-button size="sm" variant="secondary" style="margin-right:20px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
</div>
</div>
</b-popover>
<ContextMenu ref="menu">
<template #menu="{ contextData }">
<ContextMenuItem
@click="
moveEntry($event, contextData)
$refs.menu.close()
"
>
<b-form-group label-cols="10" content-cols="2" class="text-nowrap m-0 mr-2">
<template #label>
<a class="dropdown-item p-2" href="#"><i class="fas fa-cubes"></i> {{ $t("MoveCategory", { category: categoryName(shopcat) }) }}</a>
</template>
<div @click.prevent.stop @mouseup.prevent.stop>
<b-form-select class="mt-2 border-0" :options="shopping_categories" text-field="name" value-field="id" v-model="shopcat"></b-form-select>
</div>
</b-form-group>
</ContextMenuItem>
<ContextMenuItem
@click="
$refs.menu.close()
onHand(contextData)
"
>
<a class="dropdown-item p-2" href="#"><i class="fas fa-clipboard-check"></i> {{ $t("OnHand") }}</a>
</ContextMenuItem>
<ContextMenuItem
@click="
$refs.menu.close()
delayThis(contextData)
"
>
<b-form-group label-cols="10" content-cols="2" class="text-nowrap m-0 mr-2">
<template #label>
<a class="dropdown-item p-2" href="#"><i class="far fa-hourglass"></i> {{ $t("DelayFor", { hours: delay }) }}</a>
</template>
<b-form-input class="mt-2" min="0" type="number" v-model="delay"></b-form-input>
</b-form-group>
</ContextMenuItem>
<ContextMenuItem
@click="
$refs.menu.close()
ignoreThis(contextData)
"
>
<a class="dropdown-item p-2" href="#"><i class="fas fa-ban"></i> {{ $t("IgnoreThis", { food: foodName(contextData) }) }}</a>
</ContextMenuItem>
<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>
</ContextMenuItem>
</template>
</ContextMenu>
</div>
</template>
<script>
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import ContextMenu from "@/components/ContextMenu/ContextMenu"
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import ShoppingLineItem from "@/components/ShoppingLineItem"
import { ApiMixin, getUserPreference } from "@/utils/utils"
import { ApiApiFactory } from "@/utils/openapi/api"
import { StandardToasts, makeToast } from "@/utils/utils"
Vue.use(BootstrapVue)
export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: "ChecklistView",
mixins: [ApiMixin],
components: { ContextMenu, ContextMenuItem, ShoppingLineItem },
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
items: [],
group_by: "category", //TODO create setting to switch group_by
group_by_choices: ["created_by", "category", "recipe"],
supermarkets: [],
shopping_categories: [],
selected_supermarket: undefined,
show_undefined_categories: true,
supermarket_categories_only: false,
shopcat: null,
delay: 0, // user default
show_modal: false,
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
loading: true,
}
},
computed: {
Sections() {
function getKey(item, group_by, x) {
switch (group_by) {
case "category":
return item?.food?.supermarket_category?.name ?? x
case "created_by":
return item?.created_by?.username ?? x
case "recipe":
return item?.recipe_mealplan?.recipe_name ?? x
}
}
const groups = {}
this.items
let shopping_list = this.items
if (this.selected_supermarket && this.supermarket_categories_only) {
let shopping_categories = this.supermarkets // category IDs configured on supermarket
.map((x) => x.category_to_supermarket)
.flat()
.map((x) => x.category.id)
shopping_list = shopping_list.filter((x) => shopping_categories.includes(x?.food?.supermarket_category?.id))
} else if (!this.show_undefined_categories) {
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
}
shopping_list.forEach((item) => {
let key = getKey(item, this.group_by, this.$t("Undefined"))
// first level of dict is done/not done
if (!groups[item.checked]) groups[item.checked] = {}
// second level of dict is this.group_by selection
if (!groups[item.checked][key]) groups[item.checked][key] = {}
// third level of dict is the food
if (groups[item.checked][key][item.food.name]) {
groups[item.checked][key][item.food.name].push(item)
} else {
groups[item.checked][key][item.food.name] = [item]
}
})
console.log(groups)
return groups
},
Fields() {
switch (this.group_by) {
case "category":
return this.fields.filter((x) => x !== "category")
case "recipe":
return this.fields.filter((x) => x.key !== "recipe_mealplan.name")
}
return this.fields
},
},
watch: {
selected_supermarket(newVal, oldVal) {
this.getItems()
},
},
mounted() {
// value is passed from lists.py
this.getItems()
this.getSupermarkets()
this.getShoppingCategories()
this.hours = getUserPreference("shopping_delay") || 1
},
methods: {
// this.genericAPI inherited from ApiMixin
getItems: function(params = {}) {
this.loading = true
params = {
supermarket: this.selected_supermarket,
}
params.options = { query: { recent: 1 } } // returns extended values in API response
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
.then((results) => {
console.log(results.data)
if (results.data?.length) {
this.items = results.data
} else {
console.log("no data returned")
}
this.loading = false
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
getSupermarkets: function() {
this.genericAPI(this.Models.SUPERMARKET, this.Actions.LIST).then((result) => {
this.supermarkets = result.data
})
},
getShoppingCategories: function() {
this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.LIST).then((result) => {
this.shopping_categories = result.data
})
},
getThis: function(id) {
return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id })
},
saveThis: function(thisItem, toast = true) {
if (!thisItem?.id) {
// if there is no item id assume it's a new item
this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.CREATE, thisItem)
.then((result) => {
// this.items = result.data - refresh the list here
if (toast) {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
}
})
.catch((err) => {
console.log(err)
if (toast) {
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
}
})
} else {
this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.UPDATE, thisItem)
.then((result) => {
// this.refreshThis(thisItem.id) refresh the list here
if (toast) {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
}
})
.catch((err) => {
console.log(err, err.response)
if (toast) {
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
}
})
}
},
getRecipe: function(item) {
// change to get pop up card? maybe same for unit and food?
},
deleteThis: function(id) {
this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.DELETE, { id: id })
.then((result) => {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
})
},
// formatAmount: function(value) {
// return value[1][0].amount
// },
// formatCategory: function(value) {
// return value[1][0]?.name ?? this.$t("Undefined")
// },
// formatChecked: function(value) {
// return false
// },
// formatFood: function(value) {
// return value[1][0]?.food?.name ?? this.$t("Undefined")
// },
// formatUnit: function(value) {
// return value[1][0]?.unit?.name ?? this.$t("Undefined")
// },
// formatRecipe: function(value) {
// return value[1][0]?.recipe_mealplan?.recipe_name ?? this.$t("Undefined")
// },
// formatNotes: function(value) {
// return [value[1][0]?.recipe_mealplan?.mealplan_note, value?.ingredient_note].filter(String).join("\n")
// },
// formatDate: function(datetime) {
// if (!datetime) {
// return
// }
// return Intl.DateTimeFormat(window.navigator.language, { dateStyle: "short", timeStyle: "short" }).format(Date.parse(datetime))
// },
toggleChecked: function(item, refresh = false) {
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
item.checked = !item.checked
if (item.checked) {
item.completed_at = new Date().toISOString()
}
this.saveThis(item, false)
},
sectionID: function(a, b) {
return (a + b).replace(/\W/g, "")
},
openContextMenu(e, value) {
this.$refs.menu.open(e, value)
},
moveEntry: function(e, item) {
if (!e) {
makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning")
}
// TODO update API to warn that category is inherited
let food = {
id: item?.[0]?.food?.id ?? item?.food?.id,
supermarket_category: this.shopping_categories.filter((x) => x?.id === e)?.[0],
}
console.log("food", food, "event", e, "item", item)
this.genericAPI(this.Models.FOOD, this.Actions.UPDATE, food)
.then((result) => {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
// TODO this needs to change order of saved item to match category or category sort needs to be local instead of based on API
this.items
.filter((x) => x.food.id == food.id)
.forEach((y) => {
y.food.supermarket_category = food.supermarket_category
this.updateEntry(y)
})
})
.catch((err) => {
console.log(err, Object.keys(err))
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
this.shopcat = null
},
updateEntry: function(newItem) {
let idx = this.items.indexOf((x) => x.id === newItem.id)
Vue.set(this.items, idx, newItem)
},
foodName: function(value) {
console.log(value?.food?.name ?? value?.[0]?.food?.name ?? "")
return value?.food?.name ?? value?.[0]?.food?.name ?? ""
},
ignoreThis: function(item) {
return item
},
onHand: function(item) {
return item
},
delayThis: function(item) {
return item
},
categoryName: function(value) {
return this.shopping_categories.filter((x) => x.id == value)[0]?.name ?? ""
},
stop: function(e) {
e.stopPropagation() // default @click.stop not working
e.preventDefault() // default @click.stop not working
},
},
}
</script>
<!--style src="vue-multiselect/dist/vue-multiselect.min.css"></style-->
<style>
.rotate {
-moz-transition: all 0.25s linear;
-webkit-transition: all 0.25s linear;
transition: all 0.25s linear;
}
.btn[aria-expanded="true"] > .rotate {
-moz-transform: rotate(90deg);
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.float-top {
padding-bottom: -3em;
margin-bottom: -3em;
}
</style>

View File

@ -1,6 +1,8 @@
<template>
<!-- add alert at top if offline -->
<!-- get autosync time from preferences and put fetching checked items on timer -->
<!-- allow reordering or items -->
<div id="app" style="margin-bottom: 4vh">
<b-alert :show="!online" dismissible class="small float-up" variant="warning">{{ $t("OfflineAlert") }}</b-alert>
<div class="row float-top">
<div class="offset-md-10 col-md-2 no-gutter text-right">
<b-button variant="link" class="px-0">
@ -11,15 +13,15 @@
</b-button>
</div>
</div>
<b-tabs content-class="mt-3">
<!-- shopping list tab -->
<b-tab :title="$t('ShoppingList')" active>
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("ShoppingList") }} </template>
<div class="container">
<div class="row">
<div class="col col-md-12">
<!-- TODO add spinner -->
<div role="tablist" v-if="items && items.length > 0">
<!-- WARNING: all data in the table should be considered read-only, don't change any data through table bindings -->
<div class="row justify-content-md-center w-75" v-if="entrymode">
<div class="col col-md-2 "><b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input></div>
<div class="col col-md-3">
@ -72,7 +74,12 @@
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
<!-- passing an array of values to the table grouped by Food -->
<div v-for="(entries, x) in Object.entries(s)" :key="x">
<ShoppingLineItem :entries="entries[1]" :groupby="group_by" @open-context-menu="openContextMenu" @update-checkbox="updateChecked" />
<ShoppingLineItem
:entries="entries[1]"
:groupby="group_by"
@open-context-menu="openContextMenu"
@update-checkbox="updateChecked"
></ShoppingLineItem>
</div>
</div>
</div>
@ -82,11 +89,11 @@
</div>
</div>
</b-tab>
<!-- recipe tab -->
<b-tab :title="$t('Recipes')">
<table class="table w-75">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">{{ $t("Meal_Plan") }}</th>
<th scope="col">{{ $t("Recipe") }}</th>
<th scope="col">{{ $t("Servings") }}</th>
@ -94,6 +101,7 @@
</tr>
</thead>
<tr v-for="r in Recipes" :key="r.list_recipe">
<td>{{ r.list_recipe }}</td>
<td>{{ r.recipe_mealplan.name }}</td>
<td>{{ r.recipe_mealplan.recipe_name }}</td>
<td class="block-inline">
@ -105,124 +113,15 @@
</tr>
</table>
</b-tab>
<!-- settings tab -->
<b-tab :title="$t('Settings')">
<div class="row">
<div class="col col-md-4 ">
<b-card class="no-body">
<div class="row">
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.mealplan_autoadd_shopping" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">{{ $t("mealplan_autoadd_shopping_desc") }}</em>
</div>
</div>
<div v-if="settings.mealplan_autoadd_shopping">
<div class="row">
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.mealplan_autoexclude_onhand" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">{{ $t("mealplan_autoadd_shopping_desc") }}</em>
</div>
</div>
</div>
<div v-if="settings.mealplan_autoadd_shopping">
<div class="row">
<div class="col col-md-6">{{ $t("mealplan_autoinclude_related") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.mealplan_autoinclude_related" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("mealplan_autoinclude_related_desc") }}
</em>
</div>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("shopping_share") }}</div>
<div class="col col-md-6 text-right">
<generic-multiselect
size="sm"
@change="
settings.shopping_share = $event.val
saveSettings()
"
:model="Models.USER"
:initial_selection="settings.shopping_share"
label="username"
:multiple="true"
:allow_create="false"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('User')"
/>
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">{{ $t("shopping_share_desc") }}</em>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("shopping_auto_sync") }}</div>
<div class="col col-md-6 text-right">
<input type="number" size="sm" v-model="settings.shopping_auto_sync" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("shopping_auto_sync_desc") }}
</em>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.filter_to_supermarket" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("filter_to_supermarket_desc") }}
</em>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("default_delay") }}</div>
<div class="col col-md-6 text-right">
<input type="number" size="sm" min="1" v-model="settings.default_delay" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("default_delay_desc") }}
</em>
</div>
</div>
</b-card>
</div>
<div class="col col-md-8">
<b-card class=" no-body">
put the supermarket stuff here<br />
-add supermarkets<br />
-add supermarket categories<br />
-sort supermarket categories<br />
</b-card>
</div>
</div>
These are the settings <br />-sort supermarket categories<br />
-add supermarket categories<br />
- add supermarkets autosync time<br />
autosync on/off<br />
always restrict supermarket to categories?<br />
when restricted or filterd - give visual indication<br />
how long to defer shopping - default tomorrow <br />
always override inheritance
</b-tab>
</b-tabs>
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
@ -243,10 +142,9 @@
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
</b-form-group>
</div>
<div class="row " style="margin-top: 1vh;min-width:300px">
<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="secondary" class="mr-3" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
<div class="row" style="margin-top: 1vh;min-width:300px">
<div class="col-12" style="text-align: right;">
<b-button size="sm" variant="secondary" style="margin-right:20px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
</div>
</div>
</b-popover>
@ -332,6 +230,8 @@ import { StandardToasts, makeToast } from "@/utils/utils"
Vue.use(BootstrapVue)
export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: "ShoppingListView",
mixins: [ApiMixin],
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect },
@ -340,7 +240,7 @@ export default {
return {
// this.Models and this.Actions inherited from ApiMixin
items: [],
group_by: "category",
group_by: "category", //TODO create setting to switch group_by
group_by_choices: ["created_by", "category", "recipe"],
supermarkets: [],
shopping_categories: [],
@ -348,25 +248,13 @@ export default {
show_undefined_categories: true,
supermarket_categories_only: false,
shopcat: null,
delay: 0,
settings: {
shopping_auto_sync: 0,
default_delay: 4,
mealplan_autoadd_shopping: false,
mealplan_autoinclude_related: false,
mealplan_autoexclude_onhand: true,
filter_to_supermarket: false,
},
autosync_id: undefined,
auto_sync_running: false,
delay: 0, // user default
show_delay: false,
show_modal: false,
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
loading: true,
entrymode: false,
new_item: { amount: 1, unit: undefined, food: undefined },
online: true,
}
},
computed: {
@ -401,16 +289,16 @@ export default {
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
}
let groups = { false: {}, true: {} } // force unchecked to always be first
const groups = { false: {}, true: {} } // force unchecked to always be first
if (this.selected_supermarket) {
let super_cats = this.supermarkets
.filter((x) => x.id === this.selected_supermarket)
.map((x) => x.category_to_supermarket)
.flat()
.map((x) => x.category.name)
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
groups["false"][cat.name] = {}
groups["true"][cat.name] = {}
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).foreach((cat) => {
groups.false[cat.name] = {}
groups.true[cat.name] = {}
})
} else {
this.shopping_categories.forEach((cat) => {
@ -443,7 +331,7 @@ export default {
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
},
filterApplied() {
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
},
Recipes() {
return [...new Map(this.items.filter((x) => x.list_recipe).map((item) => [item["list_recipe"], item])).values()]
@ -451,49 +339,24 @@ export default {
},
watch: {
selected_supermarket(newVal, oldVal) {
this.supermarket_categories_only = this.settings.filter_to_supermarket
},
"settings.filter_to_supermarket": function(newVal, oldVal) {
this.supermarket_categories_only = this.settings.filter_to_supermarket
},
"settings.shopping_auto_sync": function(newVal, oldVal) {
clearInterval(this.autosync_id)
this.autosync_id = undefined
if (!newVal) {
window.removeEventListener("online", this.updateOnlineStatus)
window.removeEventListener("offline", this.updateOnlineStatus)
return
} else if (oldVal === 0 && newVal > 0) {
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
}
this.autosync_id = setInterval(() => {
if (this.online && !this.auto_sync_running) {
this.auto_sync_running = true
this.getShoppingList(true)
}
}, this.settings.shopping_auto_sync * 1000)
this.getShoppingList()
},
},
mounted() {
// value is passed from lists.py
this.getShoppingList()
this.getSupermarkets()
this.getShoppingCategories()
this.settings = getUserPreference()
this.delay = this.settings.default_delay || 4
this.supermarket_categories_only = this.settings.filter_to_supermarket
if (this.settings.shopping_auto_sync) {
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
}
this.delay = getUserPreference("default_delay") || 4
},
methods: {
// this.genericAPI inherited from ApiMixin
addItem() {
console.log(this.new_item)
let api = new ApiApiFactory()
api.createShoppingListEntry(this.new_item)
.then((results) => {
console.log(results)
if (results?.data) {
this.items.push(results.data)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
@ -510,13 +373,6 @@ export default {
categoryName: function(value) {
return this.shopping_categories.filter((x) => x.id == value)[0]?.name ?? ""
},
resetFilters: function() {
this.selected_supermarket = undefined
this.supermarket_categories_only = this.settings.filter_to_supermarket
this.show_undefined_categories = true
this.group_by = "category"
this.show_delay = false
},
delayThis: function(item) {
let entries = []
let promises = []
@ -580,45 +436,40 @@ export default {
this.shopping_categories = result.data
})
},
getShoppingList: function(autosync = false) {
let params = {}
params.supermarket = this.selected_supermarket
params.options = { query: { recent: 1 } }
if (autosync) {
params.options.query["autosync"] = 1
} else {
this.loading = true
getShoppingList: function(params = {}) {
this.loading = true
params = {
supermarket: this.selected_supermarket,
}
params.options = { query: { recent: 1 } }
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
.then((results) => {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
} else {
console.log("no data returned")
}
this.loading = false
console.log(results.data)
if (results.data?.length) {
this.items = results.data
} else {
this.mergeShoppingList(results.data)
console.log("no data returned")
}
this.loading = false
})
.catch((err) => {
console.log(err)
if (!autosync) {
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
}
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
getSupermarkets: function() {
let api = new ApiApiFactory()
api.listSupermarkets().then((result) => {
this.supermarkets = result.data
console.log(this.supermarkets)
})
},
getThis: function(id) {
return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id })
},
getRecipe: function(item) {
// change to get pop up card? maybe same for unit and food?
},
ignoreThis: function(item) {
let food = {
id: item?.[0]?.food.id ?? item.food.id,
@ -626,17 +477,6 @@ export default {
}
this.updateFood(food, "ignore_shopping")
},
mergeShoppingList: function(data) {
this.items.map((x) =>
data.map((y) => {
if (y.id === x.id) {
x.checked = y.checked
return x
}
})
)
this.auto_sync_running = false
},
moveEntry: function(e, item) {
if (!e) {
makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning")
@ -671,17 +511,6 @@ export default {
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
this.$refs.menu.open(e, value)
},
saveSettings: function() {
let api = ApiApiFactory()
api.partialUpdateUserPreference(this.settings.user, this.settings)
.then((result) => {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
},
saveThis: function(thisItem, toast = true) {
let api = new ApiApiFactory()
if (!thisItem?.id) {
@ -707,6 +536,7 @@ export default {
})
.catch((err) => {
console.log(err, err.response)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
}
@ -716,17 +546,15 @@ export default {
},
updateChecked: function(update) {
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
let promises = []
update.entries.forEach((x) => {
console.log({ id: x, checked: update.checked })
promises.push(this.saveThis({ id: x, checked: update.checked }, false))
let item = this.items.filter((entry) => entry.id == x)[0]
Vue.set(item, "checked", update.checked)
if (update.checked) {
Vue.set(item, "completed_at", new Date().toISOString())
} else {
Vue.set(item, "completed_at", undefined)
}
Vue.set(item, "completed_at", new Date().toISOString())
})
Promise.all(promises).catch((err) => {
console.log(err, err.response)
@ -769,14 +597,6 @@ export default {
this.getShoppingList()
})
},
updateOnlineStatus(e) {
const { type } = e
this.online = type === "online"
},
beforeDestroy() {
window.removeEventListener("online", this.updateOnlineStatus)
window.removeEventListener("offline", this.updateOnlineStatus)
},
},
}
</script>
@ -798,8 +618,4 @@ export default {
padding-bottom: -3em;
margin-bottom: -3em;
}
.float-up {
padding-top: -3em;
margin-top: -3em;
}
</style>

View File

@ -2,12 +2,12 @@
<!-- add alert at top if offline -->
<!-- get autosync time from preferences and put fetching checked items on timer -->
<!-- allow reordering or items -->
<div id="app">
<div id="shopping_line_item">
<div class="col-12">
<div class="row" :class="{ 'text-muted': formatChecked }">
<div class="row">
<div class="col col-md-1">
<div style="position: static;" class=" btn-group">
<div class="dropdown b-dropdown position-static">
<div class="dropdown b-dropdown position-static inline-block">
<button
aria-haspopup="true"
aria-expanded="false"
@ -18,9 +18,8 @@
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
<input type="checkbox" class="text-right mx-3 mt-2" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</div>
<b-button class="btn far text-body text-decoration-none" variant="link" @click="checkboxChanged()" :class="formatChecked ? 'fa-check-square' : 'fa-square'" />
</div>
<div class="col col-md-1">{{ formatAmount }}</div>
<div class="col col-md-1">{{ formatUnit }}</div>
@ -36,8 +35,8 @@
</div>
<div class="card no-body" v-if="showDetails">
<div v-for="(e, z) in entries" :key="z">
<div class="row ml-2 small" v-if="formatOneMealPlan(e)">
<div class="col-md-4 overflow-hidden text-nowrap">
<div class="row ml-2 small">
<div class="col-md-4 overflow-hidden text-nowrap">
<button
aria-haspopup="true"
aria-expanded="false"
@ -51,12 +50,15 @@
</button>
</div>
<div class="col-md-4 text-muted">{{ formatOneMealPlan(e) }}</div>
<div class="col-md-4 text-muted">{{ formatOneCreatedBy(e) }}</div>
<div class="col-md-4 text-muted text-right">{{ formatOneCreatedBy(e) }}</div>
</div>
<div class="row ml-2 small">
<div class="col-md-4 offset-md-8 text-muted text-right">{{ formatOneCompletedAt(e) }}</div>
</div>
<div class="row ml-2 light">
<div class="col-sm-1 text-nowrap">
<div style="position: static;" class=" btn-group ">
<div class="dropdown b-dropdown position-static">
<div class="dropdown b-dropdown position-static inline-block">
<button
aria-haspopup="true"
aria-expanded="false"
@ -67,14 +69,8 @@
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
<input type="checkbox" class="text-right mx-3 mt-2" :checked="e.checked" @change="updateChecked($event, e)" />
</div>
<b-button
class="btn far text-body text-decoration-none"
variant="link"
@click="checkboxChanged(e)"
:class="formatOneChecked(e) ? 'fa-check-square' : 'fa-square'"
/>
</div>
<div class="col-sm-1">{{ formatOneAmount(e) }}</div>
<div class="col-sm-2">{{ formatOneUnit(e) }}</div>
@ -84,15 +80,26 @@
<div class="small" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
</div>
</div>
<hr class="w-75" />
</div>
</div>
<hr class="m-1" />
</div>
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width:300">
<template #menu="{ contextData }">
<ContextMenuItem><RecipeCard :recipe="contextData" :detail="false" v-if="recipe"></RecipeCard></ContextMenuItem
></template>
<template #menu="{ contextData }" v-if="recipe">
<ContextMenuItem><RecipeCard :recipe="contextData" :detail="false"></RecipeCard></ContextMenuItem>
<ContextMenuItem @click="$refs.menu.close()">
<b-form-group label-cols="9" content-cols="3" class="text-nowrap m-0 mr-2">
<template #label>
<a class="dropdown-item p-2" href="#"><i class="fas fa-pizza-slice"></i> {{ $t("Servings") }}</a>
</template>
<div @click.prevent.stop>
<b-form-input class="mt-2" min="0" type="number" v-model="servings"></b-form-input>
</div>
</b-form-group>
</ContextMenuItem>
</template>
</ContextMenu>
</div>
</template>
@ -124,6 +131,7 @@ export default {
return {
showDetails: false,
recipe: undefined,
servings: 1,
}
},
computed: {
@ -134,7 +142,7 @@ export default {
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
},
formatChecked: function() {
return false
return this.entries.map((x) => x.checked).every((x) => x === true)
},
formatHint: function() {
if (this.groupby == "recipe") {
@ -165,7 +173,9 @@ export default {
},
},
watch: {},
mounted() {},
mounted() {
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
},
methods: {
// this.genericAPI inherited from ApiMixin
@ -175,16 +185,6 @@ export default {
}
return Intl.DateTimeFormat(window.navigator.language, { dateStyle: "short", timeStyle: "short" }).format(Date.parse(datetime))
},
checkboxChanged: function() {
console.log("click!")
// item.checked = !item.checked
// if (item.checked) {
// item.completed_at = new Date().toISOString()
// }
// this.saveThis(item, false)
// this.$refs.table.refresh()
},
formatOneAmount: function(item) {
return item?.amount ?? 1
},
@ -194,6 +194,12 @@ export default {
formatOneCategory: function(item) {
return item?.food?.supermarket_category?.name
},
formatOneCompletedAt: function(item) {
if (!item.completed_at) {
return ""
}
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
},
formatOneFood: function(item) {
return item.food.name
},
@ -223,6 +229,14 @@ export default {
this.$refs.recipe_card.open(e, recipe)
})
},
updateChecked: function(e, item) {
if (!item) {
let update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
this.$emit("update-checkbox", update)
} else {
this.$emit("update-checkbox", { id: item.id, checked: !item.checked })
}
},
},
}
</script>

View File

@ -91,9 +91,9 @@ module.exports = {
},
// TODO make this conditional on .env DEBUG = FALSE
config.optimization.minimize(false)
);
)
config.plugin('BundleTracker').use(BundleTracker, [{relativePath: true, path: '../vue/'}]);
config.plugin("BundleTracker").use(BundleTracker, [{ relativePath: true, path: "../vue/" }])
config.resolve.alias.set("__STATIC__", "static")