mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-03 05:11:31 -05:00
shopping list ux improvements
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,172 +1,173 @@
|
|||||||
<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>
|
||||||
<generic-multiselect
|
<generic-multiselect
|
||||||
@change="new_value = $event.val"
|
@change="new_value = $event.val"
|
||||||
@remove="new_value = undefined"
|
@remove="new_value = undefined"
|
||||||
:initial_selection="initialSelection"
|
:initial_selection="initialSelection"
|
||||||
:model="model"
|
:model="model"
|
||||||
:multiple="useMultiple"
|
:multiple="useMultiple"
|
||||||
:sticky_options="sticky_options"
|
:sticky_options="sticky_options"
|
||||||
:allow_create="form.allow_create"
|
:allow_create="form.allow_create"
|
||||||
:create_placeholder="createPlaceholder"
|
:create_placeholder="createPlaceholder"
|
||||||
:clear="clear"
|
:clear="clear"
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||||
:placeholder="modelName"
|
:placeholder="modelName"
|
||||||
@new="addNew"
|
@new="addNew"
|
||||||
>
|
>
|
||||||
</generic-multiselect>
|
</generic-multiselect>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import { StandardToasts, ApiMixin } from "@/utils/utils"
|
import {StandardToasts, ApiMixin} from "@/utils/utils"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "LookupInput",
|
name: "LookupInput",
|
||||||
components: { GenericMultiselect },
|
components: {GenericMultiselect},
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
props: {
|
props: {
|
||||||
form: {
|
form: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
},
|
|
||||||
model: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
},
|
|
||||||
show_label: { type: Boolean, default: true },
|
|
||||||
clear: { type: Number },
|
|
||||||
},
|
},
|
||||||
data() {
|
model: {
|
||||||
return {
|
type: Object,
|
||||||
new_value: undefined,
|
default() {
|
||||||
field: undefined,
|
return undefined
|
||||||
label: undefined,
|
},
|
||||||
sticky_options: undefined,
|
},
|
||||||
first_run: true,
|
class_list: {type: String, default: "mb-3"},
|
||||||
|
show_label: {type: Boolean, default: true},
|
||||||
|
clear: {type: Number},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
new_value: undefined,
|
||||||
|
field: undefined,
|
||||||
|
label: undefined,
|
||||||
|
sticky_options: undefined,
|
||||||
|
first_run: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.new_value = this.form?.value
|
||||||
|
this.field = this.form?.field ?? "You Forgot To Set Field Name"
|
||||||
|
this.label = this.form?.label ?? ""
|
||||||
|
this.sticky_options = this.form?.sticky_options ?? []
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
modelName() {
|
||||||
|
return this?.model?.name ?? this.$t("Search")
|
||||||
|
},
|
||||||
|
useMultiple() {
|
||||||
|
return this.form?.multiple || this.form?.ordered || false
|
||||||
|
},
|
||||||
|
initialSelection() {
|
||||||
|
let this_value = this.new_value
|
||||||
|
let arrayValues = undefined
|
||||||
|
// multiselect is expect to get an array of objects - make sure it gets one
|
||||||
|
if (Array.isArray(this_value)) {
|
||||||
|
arrayValues = this_value
|
||||||
|
} else if (!this_value) {
|
||||||
|
arrayValues = []
|
||||||
|
} else if (typeof this_value === "object") {
|
||||||
|
arrayValues = [this_value]
|
||||||
|
} else {
|
||||||
|
arrayValues = [{id: -1, name: this_value}]
|
||||||
|
}
|
||||||
|
if (this.form?.ordered && this.first_run && arrayValues.length > 0) {
|
||||||
|
return this.flattenItems(arrayValues)
|
||||||
|
} else {
|
||||||
|
return arrayValues
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPlaceholder() {
|
||||||
|
return this.$t("Create_New_" + this?.model?.name)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"form.value": function (newVal, oldVal) {
|
||||||
|
this.new_value = newVal
|
||||||
|
},
|
||||||
|
new_value: function () {
|
||||||
|
let x = this?.new_value
|
||||||
|
// pass the unflattened attributes that can be restored when ready to save/update
|
||||||
|
if (this.form?.ordered) {
|
||||||
|
x["__override__"] = this.unflattenItem(this?.new_value)
|
||||||
|
}
|
||||||
|
this.$root.$emit("change", this.form.field, x)
|
||||||
|
this.$emit("change", x)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addNew: function (e) {
|
||||||
|
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||||
|
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||||
|
this.genericAPI(this.model, this.Actions.CREATE, {name: e})
|
||||||
|
.then((result) => {
|
||||||
|
this.new_value = result.data
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||||
|
flattenItems: function (itemlist) {
|
||||||
|
let flat_items = []
|
||||||
|
let item = undefined
|
||||||
|
let label = this.form.list_label.split("::")
|
||||||
|
itemlist.forEach((x) => {
|
||||||
|
item = {}
|
||||||
|
for (const [k, v] of Object.entries(x)) {
|
||||||
|
if (k == label[0]) {
|
||||||
|
item["id"] = v.id
|
||||||
|
item[label[1]] = v[label[1]]
|
||||||
|
} else {
|
||||||
|
item[this.form.field + "__" + k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
flat_items.push(item)
|
||||||
|
})
|
||||||
|
this.first_run = false
|
||||||
|
return flat_items
|
||||||
},
|
},
|
||||||
mounted() {
|
unflattenItem: function (itemList) {
|
||||||
this.new_value = this.form?.value
|
let unflat_items = []
|
||||||
this.field = this.form?.field ?? "You Forgot To Set Field Name"
|
let item = undefined
|
||||||
this.label = this.form?.label ?? ""
|
let this_label = undefined
|
||||||
this.sticky_options = this.form?.sticky_options ?? []
|
let label = this.form.list_label.split("::")
|
||||||
},
|
let order = 0
|
||||||
computed: {
|
itemList.forEach((x) => {
|
||||||
modelName() {
|
item = {}
|
||||||
return this?.model?.name ?? this.$t("Search")
|
item[label[0]] = {}
|
||||||
},
|
for (const [k, v] of Object.entries(x)) {
|
||||||
useMultiple() {
|
switch (k) {
|
||||||
return this.form?.multiple || this.form?.ordered || false
|
case "id":
|
||||||
},
|
item[label[0]]["id"] = v
|
||||||
initialSelection() {
|
break
|
||||||
let this_value = this.new_value
|
case label[1]:
|
||||||
let arrayValues = undefined
|
item[label[0]][label[1]] = v
|
||||||
// multiselect is expect to get an array of objects - make sure it gets one
|
break
|
||||||
if (Array.isArray(this_value)) {
|
default:
|
||||||
arrayValues = this_value
|
this_label = k.replace(this.form.field + "__", "")
|
||||||
} else if (!this_value) {
|
}
|
||||||
arrayValues = []
|
}
|
||||||
} else if (typeof this_value === "object") {
|
item["order"] = order
|
||||||
arrayValues = [this_value]
|
order++
|
||||||
} else {
|
unflat_items.push(item)
|
||||||
arrayValues = [{ id: -1, name: this_value }]
|
})
|
||||||
}
|
return unflat_items
|
||||||
if (this.form?.ordered && this.first_run && arrayValues.length > 0) {
|
|
||||||
return this.flattenItems(arrayValues)
|
|
||||||
} else {
|
|
||||||
return arrayValues
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createPlaceholder() {
|
|
||||||
return this.$t("Create_New_" + this?.model?.name)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
"form.value": function (newVal, oldVal) {
|
|
||||||
this.new_value = newVal
|
|
||||||
},
|
|
||||||
new_value: function () {
|
|
||||||
let x = this?.new_value
|
|
||||||
// pass the unflattened attributes that can be restored when ready to save/update
|
|
||||||
if (this.form?.ordered) {
|
|
||||||
x["__override__"] = this.unflattenItem(this?.new_value)
|
|
||||||
}
|
|
||||||
this.$root.$emit("change", this.form.field, x)
|
|
||||||
this.$emit("change", x)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addNew: function (e) {
|
|
||||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
|
||||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
|
||||||
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
|
|
||||||
.then((result) => {
|
|
||||||
this.new_value = result.data
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
|
||||||
flattenItems: function (itemlist) {
|
|
||||||
let flat_items = []
|
|
||||||
let item = undefined
|
|
||||||
let label = this.form.list_label.split("::")
|
|
||||||
itemlist.forEach((x) => {
|
|
||||||
item = {}
|
|
||||||
for (const [k, v] of Object.entries(x)) {
|
|
||||||
if (k == label[0]) {
|
|
||||||
item["id"] = v.id
|
|
||||||
item[label[1]] = v[label[1]]
|
|
||||||
} else {
|
|
||||||
item[this.form.field + "__" + k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flat_items.push(item)
|
|
||||||
})
|
|
||||||
this.first_run = false
|
|
||||||
return flat_items
|
|
||||||
},
|
|
||||||
unflattenItem: function (itemList) {
|
|
||||||
let unflat_items = []
|
|
||||||
let item = undefined
|
|
||||||
let this_label = undefined
|
|
||||||
let label = this.form.list_label.split("::")
|
|
||||||
let order = 0
|
|
||||||
itemList.forEach((x) => {
|
|
||||||
item = {}
|
|
||||||
item[label[0]] = {}
|
|
||||||
for (const [k, v] of Object.entries(x)) {
|
|
||||||
switch (k) {
|
|
||||||
case "id":
|
|
||||||
item[label[0]]["id"] = v
|
|
||||||
break
|
|
||||||
case label[1]:
|
|
||||||
item[label[0]][label[1]] = v
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
this_label = k.replace(this.form.field + "__", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item["order"] = order
|
|
||||||
order++
|
|
||||||
unflat_items.push(item)
|
|
||||||
})
|
|
||||||
return unflat_items
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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] }}   {{ Object.entries(formatAmount)[0][0] }}</div>
|
<div><strong>{{ Object.entries(formatAmount)[0][1] }}</strong>  
|
||||||
<div class="small" v-else v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }}   {{ 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] }}  
|
||||||
|
{{ 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) }}  
|
||||||
|
{{ 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user