Merge branch 'develop' into facet-fix

This commit is contained in:
vabene1111
2022-01-18 07:59:32 +01:00
committed by GitHub
7 changed files with 1641 additions and 1460 deletions

View File

@@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UserFile, UserPreference, ViewLog) TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation)
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
@@ -29,11 +29,52 @@ admin.site.register(User, CustomUserAdmin)
admin.site.unregister(Group) admin.site.unregister(Group)
@admin.action(description='Delete all data from a space')
def delete_space_action(modeladmin, request, queryset):
for space in queryset:
CookLog.objects.filter(space=space).delete()
ViewLog.objects.filter(space=space).delete()
ImportLog.objects.filter(space=space).delete()
BookmarkletImport.objects.filter(space=space).delete()
Comment.objects.filter(recipe__space=space).delete()
Keyword.objects.filter(space=space).delete()
Ingredient.objects.filter(space=space).delete()
Food.objects.filter(space=space).delete()
Unit.objects.filter(space=space).delete()
Step.objects.filter(space=space).delete()
NutritionInformation.objects.filter(space=space).delete()
RecipeBookEntry.objects.filter(book__space=space).delete()
RecipeBook.objects.filter(space=space).delete()
MealType.objects.filter(space=space).delete()
MealPlan.objects.filter(space=space).delete()
ShareLink.objects.filter(space=space).delete()
Recipe.objects.filter(space=space).delete()
RecipeImport.objects.filter(space=space).delete()
SyncLog.objects.filter(sync__space=space).delete()
Sync.objects.filter(space=space).delete()
Storage.objects.filter(space=space).delete()
ShoppingListEntry.objects.filter(shoppinglist__space=space).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=space).delete()
ShoppingList.objects.filter(space=space).delete()
SupermarketCategoryRelation.objects.filter(supermarket__space=space).delete()
SupermarketCategory.objects.filter(space=space).delete()
Supermarket.objects.filter(space=space).delete()
InviteLink.objects.filter(space=space).delete()
UserFile.objects.filter(space=space).delete()
Automation.objects.filter(space=space).delete()
class SpaceAdmin(admin.ModelAdmin): class SpaceAdmin(admin.ModelAdmin):
list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing') list_display = ('name', 'created_by', 'max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
search_fields = ('name', 'created_by__username') search_fields = ('name', 'created_by__username')
list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing') list_filter = ('max_recipes', 'max_users', 'max_file_storage_mb', 'allow_sharing')
date_hierarchy = 'created_at' date_hierarchy = 'created_at'
actions = [delete_space_action]
admin.site.register(Space, SpaceAdmin) admin.site.register(Space, SpaceAdmin)
@@ -128,7 +169,7 @@ def sort_tree(modeladmin, request, queryset):
class KeywordAdmin(TreeAdmin): class KeywordAdmin(TreeAdmin):
form = movenodeform_factory(Keyword) form = movenodeform_factory(Keyword)
ordering = ('space', 'path',) ordering = ('space', 'path',)
search_fields = ('name', ) search_fields = ('name',)
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting] actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]
@@ -171,13 +212,15 @@ class RecipeAdmin(admin.ModelAdmin):
admin.site.register(Recipe, RecipeAdmin) admin.site.register(Recipe, RecipeAdmin)
admin.site.register(Unit) admin.site.register(Unit)
# admin.site.register(FoodInheritField) # admin.site.register(FoodInheritField)
class FoodAdmin(TreeAdmin): class FoodAdmin(TreeAdmin):
form = movenodeform_factory(Keyword) form = movenodeform_factory(Keyword)
ordering = ('space', 'path',) ordering = ('space', 'path',)
search_fields = ('name', ) search_fields = ('name',)
actions = [sort_tree, enable_tree_sorting, disable_tree_sorting] actions = [sort_tree, enable_tree_sorting, disable_tree_sorting]

View File

@@ -165,9 +165,10 @@ class FoodInheritFieldSerializer(WritableNestedModelSerializer):
read_only_fields = ['id'] read_only_fields = ['id']
class UserPreferenceSerializer(serializers.ModelSerializer): class UserPreferenceSerializer(WritableNestedModelSerializer):
food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True) food_inherit_default = FoodInheritFieldSerializer(source='space.food_inherit', many=True, allow_null=True, required=False, read_only=True)
plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True) plan_share = UserNameSerializer(many=True, allow_null=True, required=False, read_only=True)
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
def create(self, validated_data): def create(self, validated_data):
if not validated_data.get('user', None): if not validated_data.get('user', None):

View File

@@ -45,21 +45,15 @@ def hook(request, token):
tb.save() tb.save()
if tb.chat_id == str(data['message']['chat']['id']): if tb.chat_id == str(data['message']['chat']['id']):
sl = ShoppingList.objects.filter(Q(created_by=tb.created_by)).filter(finished=False, space=tb.space).order_by('-created_at').first()
if not sl:
sl = ShoppingList.objects.create(created_by=tb.created_by, space=tb.space)
request.space = tb.space # TODO this is likely a bad idea. Verify and test request.space = tb.space # TODO this is likely a bad idea. Verify and test
request.user = tb.created_by request.user = tb.created_by
ingredient_parser = IngredientParser(request, False) ingredient_parser = IngredientParser(request, False)
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text']) amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text'])
f = ingredient_parser.get_food(ingredient) f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
sl.entries.add(
ShoppingListEntry.objects.create( ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
food=f, unit=u, amount=amount
)
)
return JsonResponse({'data': data['message']['text']}) return JsonResponse({'data': data['message']['text']})
except Exception: except Exception:
pass pass

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@
:class="{ 'border border-primary': over, shake: isError }" :class="{ 'border border-primary': over, shake: isError }"
:style="{ 'cursor:grab': useDrag }" :style="{ 'cursor:grab': useDrag }"
:draggable="useDrag" :draggable="useDrag"
@[useDrag&&`dragover`].prevent @[useDrag&&`dragover`||``].prevent
@[useDrag&&`dragenter`].prevent @[useDrag&&`dragenter`||``].prevent
@[useDrag&&`dragstart`]="handleDragStart($event)" @[useDrag&&`dragstart`||``]="handleDragStart($event)"
@[useDrag&&`dragenter`]="handleDragEnter($event)" @[useDrag&&`dragenter`||``]="handleDragEnter($event)"
@[useDrag&&`dragleave`]="handleDragLeave($event)" @[useDrag&&`dragleave`||``]="handleDragLeave($event)"
@[useDrag&&`drop`]="handleDragDrop($event)" @[useDrag&&`drop`||``]="handleDragDrop($event)"
> >
<b-row no-gutters> <b-row no-gutters>
<b-col no-gutters class="col-sm-3"> <b-col no-gutters class="col-sm-3">
@@ -27,6 +27,7 @@
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div> <div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="itemList(x)" :label="x.label" :color="x.color" /> <generic-pill v-for="x in itemTags" :key="x.field" :item_list="itemList(x)" :label="x.label" :color="x.color" />
<generic-ordered-pill <generic-ordered-pill
v-for="x in itemOrderedTags" v-for="x in itemOrderedTags"
:key="x.field" :key="x.field"
@@ -37,6 +38,7 @@
:item="item" :item="item"
@finish-action="finishAction" @finish-action="finishAction"
/> />
<div class="mt-auto mb-1" align="right"> <div class="mt-auto mb-1" align="right">
<span v-if="item[child_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800" v-on:click="$emit('item-action', { action: 'get-children', source: item })"> <span v-if="item[child_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800" v-on:click="$emit('item-action', { action: 'get-children', source: item })">
<div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div> <div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<b-form-group class="mb-3"> <b-form-group :class="class_list">
<template #label v-if="show_label"> <template #label v-if="show_label">
{{ form.label }} {{ form.label }}
</template> </template>
@@ -44,6 +44,7 @@ export default {
return undefined return undefined
}, },
}, },
class_list: {type: String, default: "mb-3"},
show_label: { type: Boolean, default: true }, show_label: { type: Boolean, default: true },
clear: { type: Number }, clear: { type: Number },
}, },
@@ -82,7 +83,7 @@ export default {
} else { } else {
arrayValues = [{ id: -1, name: this_value }] arrayValues = [{ id: -1, name: this_value }]
} }
if (this.form?.ordered && this.first_run && arrayValues.length > 0) { if (this.form?.ordered && this.first_run) {
return this.flattenItems(arrayValues) return this.flattenItems(arrayValues)
} else { } else {
return arrayValues return arrayValues

View File

@@ -1,287 +1,322 @@
<template> <template>
<div id="shopping_line_item"> <div id="shopping_line_item">
<div class="col-12"> <b-container fluid class="pr-0 pl-1 pl-md-3">
<b-container fluid> <!-- summary rows -->
<!-- summary rows --> <b-row align-h="start">
<b-row align-h="start"> <b-col cols="1" class="align-items-center d-flex">
<b-col cols="12" sm="2"> <div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
<div style="position: static" class="btn-group"> <button
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"> aria-haspopup="true"
<button aria-expanded="false"
aria-haspopup="true" type="button"
aria-expanded="false" class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
type="button" @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>
@click.stop="$emit('open-context-menu', $event, entries)" </button>
> </div>
<i class="fas fa-ellipsis-v fa-lg"></i> </b-col>
</button> <b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
</div> <input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked"
<input type="checkbox" class="text-right mx-3 mt-2" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" /> @change="updateChecked"
</div> :key="entries[0].id"/>
</b-col> </b-col>
<b-col cols="12" sm="10"> <b-col cols="8" md="9">
<b-row> <b-row class="d-flex h-100" @click.stop="$emit('open-context-menu', $event, entries)">
<b-col cols="6" sm="3"> <b-col cols="6" md="6" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
<div v-if="Object.entries(formatAmount).length == 1">{{ Object.entries(formatAmount)[0][1] }} &ensp; {{ Object.entries(formatAmount)[0][0] }}</div> <div><strong>{{ Object.entries(formatAmount)[0][1] }}</strong> &ensp;
<div class="small" v-else v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }} &ensp; {{ x[0] }}</div> {{ Object.entries(formatAmount)[0][0] }}
</b-col> </div>
</b-col>
<b-col cols="6" md="6" 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] }} &ensp;
{{ x[0] }}
</div>
</b-col>
<b-col cols="6" sm="7"> <b-col cols="6" md="3" 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="6" sm="2" data-html2canvas-ignore="true"> <b-col cols="3" data-html2canvas-ignore="true" class="align-items-center d-none d-md-flex">
<b-button size="sm" @click="showDetails = !showDetails" class="mr-2" variant="link"> <b-button size="sm" @click="showDetails = !showDetails" class="p-0 mr-0 mr-md-2 p-md-2" variant="link">
<div class="text-nowrap">{{ showDetails ? "Hide" : "Show" }} Details</div> <div class="text-nowrap"><i class="fa fa-chevron-right rotate"
</b-button> :class="showDetails ? 'rotated' : ''"></i> <span
</b-col> class="d-none d-md-inline-block">{{ $t('Details') }}</span>
</b-row> </div>
</b-col> </b-button>
</b-row> </b-col>
<b-row align-h="center"> </b-row>
<b-col cols="12"> </b-col>
<div class="small text-muted text-truncate">{{ formatHint }}</div> <b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none">
</b-col> <b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
</b-row> <div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
</b-container>
<!-- detail rows -->
<div class="card no-body" v-if="showDetails">
<b-container fluid>
<div v-for="e in entries" :key="e.id">
<b-row class="ml-2 small">
<b-col cols="6" md="4" class="overflow-hidden text-nowrap">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-link btn-sm m-0 p-0"
style="text-overflow: ellipsis"
@click.stop="openRecipeCard($event, e)"
@mouseover="openRecipeCard($event, e)"
>
{{ formatOneRecipe(e) }}
</button>
</b-col>
<b-col cols="6" md="4" class="col-md-4 text-muted">{{ formatOneMealPlan(e) }}</b-col>
<b-col cols="12" md="4" class="col-md-4 text-muted text-right overflow-hidden text-nowrap">
{{ formatOneCreatedBy(e) }}
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
</b-col>
</b-row>
<b-row class="ml-2 light">
<b-col cols="12" sm="2">
<div style="position: static" class="btn-group">
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
@click.stop="$emit('open-context-menu', $event, e)"
>
<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-col>
<b-col cols="12" sm="10">
<b-row>
<b-col cols="2" sm="2" md="1" class="text-nowrap">{{ formatOneAmount(e) }}</b-col>
<b-col cols="10" sm="4" md="2" class="text-nowrap">{{ formatOneUnit(e) }}</b-col>
<b-col cols="12" sm="6" md="4" class="text-nowrap">{{ formatOneFood(e) }}</b-col>
<b-col cols="12" sm="6" md="5">
<div class="small" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
</b-col>
</b-row>
</b-col>
</b-row>
<hr class="w-75" />
</div>
</b-container>
</div> </div>
<hr class="m-1" /> </b-button>
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
</b-row>
<b-row align-h="center" class="d-none d-md-flex">
<b-col cols="12">
<div class="small text-muted text-truncate">{{ formatHint }}</div>
</b-col>
</b-row>
</b-container>
<!-- detail rows -->
<div class="card no-body mb-1 pt-2 align-content-center ml-2" v-if="showDetails">
<b-container fluid>
<div v-for="(e, x) in entries" :key="e.id">
<b-row class="small justify-content-around">
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-link btn-sm m-0 p-0"
style="text-overflow: ellipsis"
@click.stop="openRecipeCard($event, e)"
@mouseover="openRecipeCard($event, e)">
{{ formatOneRecipe(e) }}
</button>
</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">
{{ formatOneCreatedBy(e) }}
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
</b-col>
</b-row>
<b-row align-h="start">
<b-col cols="1" class="align-items-center d-flex">
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret"
@click.stop="$emit('open-context-menu', $event, e)">
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
</b-col>
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
<b-col cols="8" md="9">
<b-row class="d-flex justify-content-around">
<b-col cols="6" md="6" class="d-flex align-items-center">
<div>{{ formatOneAmount(e) }} &ensp;
{{ formatOneUnit(e) }}
</div>
</b-col>
<b-col cols="6" md="3" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
{{ formatOneFood(e) }}
</b-col>
<b-col cols="12" class="d-flex d-md-none">
<div class="small text-muted text-truncate" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
</b-col>
</b-row>
</b-col>
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none">
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
:checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
</b-row>
<hr class="w-75" v-if="x !== entries.length -1"/>
<div class="pb-4" v-if="x === entries.length -1"></div>
</div> </div>
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300"> </b-container>
<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> </div>
<hr class="m-1" v-if="!showDetails"/>
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
<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> </template>
<script> <script>
import Vue from "vue" import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue" import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css" import "bootstrap-vue/dist/bootstrap-vue.css"
import ContextMenu from "@/components/ContextMenu/ContextMenu" import ContextMenu from "@/components/ContextMenu/ContextMenu"
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem" import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import { ApiMixin } from "@/utils/utils" import {ApiMixin} from "@/utils/utils"
import RecipeCard from "./RecipeCard.vue" import RecipeCard from "./RecipeCard.vue"
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available // TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly // or i'm capturing it incorrectly
name: "ShoppingLineItem", name: "ShoppingLineItem",
mixins: [ApiMixin], mixins: [ApiMixin],
components: { RecipeCard, ContextMenu, ContextMenuItem }, components: {RecipeCard, ContextMenu, ContextMenuItem},
props: { props: {
entries: { entries: {
type: Array, type: Array,
},
groupby: { type: String },
}, },
data() { groupby: {type: String},
return { },
showDetails: false, data() {
recipe: undefined, return {
servings: 1, showDetails: false,
recipe: undefined,
servings: 1,
}
},
computed: {
formatAmount: function () {
let amount = {}
this.entries.forEach((entry) => {
let unit = entry?.unit?.name ?? "----"
if (entry.amount) {
if (amount[unit]) {
amount[unit] += entry.amount
} else {
amount[unit] = entry.amount
}
} }
})
for (const [k, v] of Object.entries(amount)) {
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places
}
return amount
}, },
computed: { formatCategory: function () {
formatAmount: function () { return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
let amount = {} },
this.entries.forEach((entry) => { formatChecked: function () {
let unit = entry?.unit?.name ?? "----" return this.entries.map((x) => x.checked).every((x) => x === true)
if (entry.amount) { },
if (amount[unit]) { formatHint: function () {
amount[unit] += entry.amount if (this.groupby == "recipe") {
} else { return this.formatCategory
amount[unit] = entry.amount } else {
} return this.formatRecipe
} }
}) },
for (const [k, v] of Object.entries(amount)) { formatFood: function () {
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places return this.formatOneFood(this.entries[0])
} },
return amount formatUnit: function () {
}, return this.formatOneUnit(this.entries[0])
formatCategory: function () { },
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined") formatRecipe: function () {
}, if (this.entries?.length == 1) {
formatChecked: function () { return this.formatOneMealPlan(this.entries[0]) || ""
return this.entries.map((x) => x.checked).every((x) => x === true) } else {
}, let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
formatHint: function () { // return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
if (this.groupby == "recipe") {
return this.formatCategory
} else {
return this.formatRecipe
}
},
formatFood: function () {
return this.formatOneFood(this.entries[0])
},
formatUnit: function () {
return this.formatOneUnit(this.entries[0])
},
formatRecipe: function () {
if (this.entries?.length == 1) {
return this.formatOneMealPlan(this.entries[0]) || ""
} else {
let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
// return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
return mealplan_name return mealplan_name
.map((x) => { .map((x) => {
return this.formatOneMealPlan(x) return this.formatOneMealPlan(x)
})
.join(" - ")
}
},
formatNotes: function () {
if (this.entries?.length == 1) {
return this.formatOneNote(this.entries[0]) || ""
}
return ""
},
},
watch: {},
mounted() {
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
},
methods: {
// this.genericAPI inherited from ApiMixin
formatDate: function (datetime) {
if (!datetime) {
return
}
return Intl.DateTimeFormat(window.navigator.language, { dateStyle: "short", timeStyle: "short" }).format(Date.parse(datetime))
},
formatOneAmount: function (item) {
return item?.amount ?? 1
},
formatOneUnit: function (item) {
return item?.unit?.name ?? ""
},
formatOneCategory: function (item) {
return item?.food?.supermarket_category?.name
},
formatOneCompletedAt: function (item) {
if (!item.completed_at) {
return false
}
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
},
formatOneFood: function (item) {
return item.food.name
},
formatOneDelayUntil: function (item) {
if (!item.delay_until || (item.delay_until && item.checked)) {
return false
}
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
},
formatOneMealPlan: function (item) {
return item?.recipe_mealplan?.name ?? ""
},
formatOneRecipe: function (item) {
return item?.recipe_mealplan?.recipe_name ?? ""
},
formatOneNote: function (item) {
if (!item) {
item = this.entries[0]
}
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
},
formatOneCreatedBy: function (item) {
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
},
openRecipeCard: function (e, item) {
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, { id: item.recipe_mealplan.recipe }).then((result) => {
let recipe = result.data
recipe.steps = undefined
this.recipe = true
this.$refs.recipe_card.open(e, recipe)
}) })
}, .join(" - ")
updateChecked: function (e, item) { }
let update = undefined
if (!item) {
update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
} else {
update = { entries: [item], checked: !item.checked }
}
this.$emit("update-checkbox", update)
},
}, },
formatNotes: function () {
if (this.entries?.length == 1) {
return this.formatOneNote(this.entries[0]) || ""
}
return ""
},
},
watch: {},
mounted() {
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
},
methods: {
// this.genericAPI inherited from ApiMixin
formatDate: function (datetime) {
if (!datetime) {
return
}
return Intl.DateTimeFormat(window.navigator.language, {
dateStyle: "short",
timeStyle: "short"
}).format(Date.parse(datetime))
},
formatOneAmount: function (item) {
return item?.amount ?? 1
},
formatOneUnit: function (item) {
return item?.unit?.name ?? ""
},
formatOneCategory: function (item) {
return item?.food?.supermarket_category?.name
},
formatOneCompletedAt: function (item) {
if (!item.completed_at) {
return false
}
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
},
formatOneFood: function (item) {
return item.food.name
},
formatOneDelayUntil: function (item) {
if (!item.delay_until || (item.delay_until && item.checked)) {
return false
}
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
},
formatOneMealPlan: function (item) {
return item?.recipe_mealplan?.name ?? ""
},
formatOneRecipe: function (item) {
return item?.recipe_mealplan?.recipe_name ?? ""
},
formatOneNote: function (item) {
if (!item) {
item = this.entries[0]
}
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
},
formatOneCreatedBy: function (item) {
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
},
openRecipeCard: function (e, item) {
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, {id: item.recipe_mealplan.recipe}).then((result) => {
let recipe = result.data
recipe.steps = undefined
this.recipe = true
this.$refs.recipe_card.open(e, recipe)
})
},
updateChecked: function (e, item) {
let update = undefined
if (!item) {
update = {entries: this.entries.map((x) => x.id), checked: !this.formatChecked}
} else {
update = {entries: [item], checked: !item.checked}
}
this.$emit("update-checkbox", update)
},
},
} }
</script> </script>
@@ -296,4 +331,28 @@ export default {
/* left: 0; top: 50%; width: 100%; /* …with the top across the middle */ /* left: 0; top: 50%; width: 100%; /* …with the top across the middle */
/* border-bottom: 1px solid #000; /* …and with a border on the top */ /* border-bottom: 1px solid #000; /* …and with a border on the top */
/* } */ /* } */
.checkbox-control {
font-size: 0.6rem
}
.checkbox-control-mobile {
font-size: 1rem
}
.rotate {
-moz-transition: all 0.25s linear;
-webkit-transition: all 0.25s linear;
transition: all 0.25s linear;
}
.rotated {
-moz-transform: rotate(90deg);
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.unit-badge-lg {
font-size: 1rem !important;
font-weight: 500 !important;
}
</style> </style>