mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-02 04:39:54 -05:00
Merge branch 'TandoorRecipes:develop' into Auto-Planner
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- bottom button nav -->
|
||||
<div class="fixed-bottom p-1 pt-2 pl-2 pr-2 border-top text-center d-lg-none" style="background: white">
|
||||
<div class="fixed-bottom p-1 pt-2 pl-2 pr-2 border-top text-center d-lg-none d-print-none" style="background: white">
|
||||
<div class="d-flex flex-row justify-content-around">
|
||||
<div class="flex-column" v-if="show_button_1">
|
||||
<slot name="button_1">
|
||||
|
||||
412
vue/src/components/FoodEditor.vue
Normal file
412
vue/src/components/FoodEditor.vue
Normal file
@@ -0,0 +1,412 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
|
||||
<b-modal :id="id" size="xl" @hidden="cancelAction" :body-class="`pr-3 pl-3`">
|
||||
|
||||
<template v-slot:modal-title>
|
||||
<div class="row" v-if="food">
|
||||
<div class="col-12">
|
||||
<h2>{{ food.name }} <small class="text-muted" v-if="food.plural_name">{{
|
||||
food.plural_name
|
||||
}}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<b-tabs content-class="mt-3" v-if="food">
|
||||
<b-tab title="General" active>
|
||||
<b-form>
|
||||
<b-form-group :label="$t('Name')" description="">
|
||||
<b-form-input v-model="food.name"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group :label="$t('Plural')" description="">
|
||||
<b-form-input v-model="food.plural_name"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<!-- Food properties -->
|
||||
|
||||
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
|
||||
|
||||
<b-form-group :label="$t('Properties Food Amount')" description=""> <!-- TODO localize -->
|
||||
<b-form-input v-model="food.properties_food_amount"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('Properties Food Unit')" description=""> <!-- TODO localize -->
|
||||
<generic-multiselect
|
||||
@change="food.properties_food_unit = $event.val;"
|
||||
:model="Models.UNIT"
|
||||
:initial_single_selection="food.properties_food_unit"
|
||||
label="name"
|
||||
:multiple="false"
|
||||
:placeholder="$t('Unit')"
|
||||
></generic-multiselect>
|
||||
</b-form-group>
|
||||
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> {{ $t('Property Amount') }}</th> <!-- TODO localize -->
|
||||
<th> {{ $t('Property Type') }}</th> <!-- TODO localize -->
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="fp in food.properties" v-bind:key="fp.id">
|
||||
<td><input v-model="fp.property_amount" type="number"> <span
|
||||
v-if="fp.property_type">{{ fp.property_type.unit }}</span></td>
|
||||
<td>
|
||||
<generic-multiselect
|
||||
@change="fp.property_type = $event.val"
|
||||
:initial_single_selection="fp.property_type"
|
||||
label="name" :model="Models.PROPERTY_TYPE"
|
||||
:multiple="false"/>
|
||||
</td>
|
||||
<td> / <span>{{ food.properties_food_amount }} <span
|
||||
v-if="food.properties_food_unit !== null">{{
|
||||
food.properties_food_unit.name
|
||||
}}</span></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-danger btn-small" @click="deleteProperty(fp)"><i
|
||||
class="fas fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="text-center">
|
||||
<b-button-group>
|
||||
<b-btn class="btn btn-success shadow-none" @click="addProperty()"><i
|
||||
class="fa fa-plus"></i>
|
||||
</b-btn>
|
||||
<b-btn class="btn btn-secondary shadow-none" @click="addAllProperties()"><i
|
||||
class="fa fa-plus"> <i class="ml-1 fas fa-list"></i></i>
|
||||
</b-btn>
|
||||
</b-button-group>
|
||||
|
||||
</div>
|
||||
|
||||
<b-form-group :label="$t('Shopping_Category')" :description="$t('shopping_category_help')">
|
||||
<generic-multiselect
|
||||
@change="food.supermarket_category = $event.val;"
|
||||
:model="Models.SHOPPING_CATEGORY"
|
||||
:initial_single_selection="food.supermarket_category"
|
||||
label="name"
|
||||
:multiple="false"
|
||||
:allow_create="true"
|
||||
:placeholder="$t('Shopping_Category')"
|
||||
></generic-multiselect>
|
||||
</b-form-group>
|
||||
|
||||
|
||||
</b-form>
|
||||
</b-tab>
|
||||
<b-tab title="Conversions" @click="loadUnitConversions" v-if="this.food.id !== undefined">
|
||||
|
||||
<b-row v-for="uc in unit_conversions" :key="uc">
|
||||
<b-col>
|
||||
<span v-if="uc.id">
|
||||
<b-btn class="btn btn-sm" variant="danger" @click="deleteUnitConversion(uc)"><i class="fas fa-trash-alt"></i></b-btn>
|
||||
{{ uc.base_amount }}
|
||||
{{ uc.base_unit.name }}
|
||||
=
|
||||
{{ uc.converted_amount }}
|
||||
{{ uc.converted_unit.name }}
|
||||
</span>
|
||||
<b-form class="mt-1">
|
||||
<b-input-group>
|
||||
<b-input v-model="uc.base_amount" @change="uc.changed = true"></b-input>
|
||||
<b-input-group-append>
|
||||
<generic-multiselect
|
||||
@change="uc.base_unit = $event.val; uc.changed = true"
|
||||
:initial_single_selection="uc.base_unit"
|
||||
label="name" :model="Models.UNIT"
|
||||
:multiple="false"/>
|
||||
</b-input-group-append>
|
||||
|
||||
</b-input-group>
|
||||
|
||||
<b-input-group>
|
||||
<b-input v-model="uc.converted_amount" @change="uc.changed = true"></b-input>
|
||||
<b-input-group-append>
|
||||
<generic-multiselect
|
||||
@change="uc.converted_unit = $event.val; uc.changed = true"
|
||||
:initial_single_selection="uc.converted_unit"
|
||||
label="name" :model="Models.UNIT"
|
||||
:multiple="false"/>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
</b-form>
|
||||
</b-col>
|
||||
<hr style="height: 1px"/>
|
||||
</b-row>
|
||||
|
||||
<b-row>
|
||||
<b-col class="text-center">
|
||||
<b-btn variant="success" @click="addUnitConversion"><i class="fa fa-plus"></i></b-btn>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
</b-tab>
|
||||
<b-tab title="More">
|
||||
<b-form>
|
||||
|
||||
|
||||
<b-form-group :label="$t('Recipe')" :description="$t('food_recipe_help')">
|
||||
<generic-multiselect
|
||||
@change="food.recipe = $event.val;"
|
||||
:model="Models.RECIPE"
|
||||
:initial_single_selection="food.recipe"
|
||||
label="name"
|
||||
:multiple="false"
|
||||
:placeholder="$t('Recipe')"
|
||||
></generic-multiselect>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('OnHand_help')">
|
||||
<b-form-checkbox v-model="food.food_onhand">{{ $t('OnHand') }}</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('ignore_shopping_help')">
|
||||
<b-form-checkbox v-model="food.ignore_shopping">{{
|
||||
$t('Ignore_Shopping')
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<hr/>
|
||||
<!-- todo add conditions if false disable dont hide -->
|
||||
<b-form-group :label="$t('Substitutes')" :description="$t('substitute_help')">
|
||||
<generic-multiselect
|
||||
@change="food.substitute = $event.val;"
|
||||
:model="Models.FOOD"
|
||||
:initial_selection="food.substitute"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:placeholder="$t('Substitutes')"
|
||||
></generic-multiselect>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('substitute_siblings_help')">
|
||||
<b-form-checkbox v-model="food.substitute_siblings">{{
|
||||
$t('substitute_siblings')
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('InheritFields')" :description="$t('InheritFields_help')">
|
||||
<generic-multiselect
|
||||
@change="food.inherit_fields = $event.val;"
|
||||
:model="Models.FOOD_INHERIT_FIELDS"
|
||||
:initial_selection="food.inherit_fields"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:placeholder="$t('InheritFields')"
|
||||
></generic-multiselect>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('ChildInheritFields')"
|
||||
:description="$t('ChildInheritFields_help')">
|
||||
<generic-multiselect
|
||||
@change="food.child_inherit_fields = $event.val;"
|
||||
:model="Models.FOOD_INHERIT_FIELDS"
|
||||
:initial_sselection="food.child_inherit_fields"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:placeholder="$t('ChildInheritFields')"
|
||||
></generic-multiselect>
|
||||
</b-form-group>
|
||||
|
||||
<!-- TODO change to a button -->
|
||||
<b-form-group :description="$t('reset_children_help')">
|
||||
<b-form-checkbox v-model="food.reset_inherit">{{
|
||||
$t('reset_children')
|
||||
}}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
|
||||
</b-form>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</div>
|
||||
|
||||
<template v-slot:modal-footer>
|
||||
<b-button variant="primary" @click="updateFood">{{ $t('Save') }}</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect.vue";
|
||||
import {ApiMixin, formFunctions, getForm, StandardToasts} from "@/utils/utils";
|
||||
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
|
||||
export default {
|
||||
name: "FoodEditor",
|
||||
mixins: [ApiMixin],
|
||||
components: {
|
||||
GenericMultiselect
|
||||
},
|
||||
props: {
|
||||
id: {type: String, default: 'id_food_edit_modal_modal'},
|
||||
show: {required: true, type: Boolean, default: false},
|
||||
item1: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function () {
|
||||
if (this.show) {
|
||||
this.$bvModal.show(this.id)
|
||||
} else {
|
||||
this.$bvModal.hide(this.id)
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
food: undefined,
|
||||
unit_conversions: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$bvModal.show(this.id)
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
let apiClient = new ApiApiFactory()
|
||||
let pf
|
||||
if (this.item1.id !== undefined) {
|
||||
pf = apiClient.retrieveFood(this.item1.id).then((r) => {
|
||||
this.food = r.data
|
||||
this.food.properties_food_unit = {name: 'g'}
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
} else {
|
||||
this.food = {
|
||||
name: "",
|
||||
plural_name: "",
|
||||
description: "",
|
||||
shopping: false,
|
||||
recipe: null,
|
||||
properties: [],
|
||||
properties_food_amount: 100,
|
||||
properties_food_unit: {name: 'g'},
|
||||
food_onhand: false,
|
||||
supermarket_category: null,
|
||||
parent: null,
|
||||
numchild: 0,
|
||||
inherit_fields: [],
|
||||
ignore_shopping: false,
|
||||
substitute: [],
|
||||
substitute_siblings: false,
|
||||
substitute_children: false,
|
||||
substitute_onhand: false,
|
||||
child_inherit_fields: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateFood: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
if (this.food.id !== undefined) {
|
||||
apiClient.updateFood(this.food.id, this.food).then((r) => {
|
||||
this.food = r.data
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
} else {
|
||||
apiClient.createFood(this.food).then((r) => {
|
||||
this.food = r.data
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
}
|
||||
|
||||
this.unit_conversions.forEach(uc => {
|
||||
if (uc.changed === true) {
|
||||
if (uc.id === undefined) {
|
||||
apiClient.createUnitConversion(uc).then(r => {
|
||||
uc = r.data
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err, true)
|
||||
})
|
||||
} else {
|
||||
apiClient.updateUnitConversion(uc.id, uc).then(r => {
|
||||
uc = r.data
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
addProperty: function () {
|
||||
this.food.properties.push({property_type: null, property_amount: 0})
|
||||
},
|
||||
addAllProperties: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listPropertyTypes().then(r => {
|
||||
r.data.forEach(x => {
|
||||
this.food.properties.push({property_type: x, property_amount: 0})
|
||||
})
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
},
|
||||
deleteProperty: function (p) {
|
||||
this.food.properties = this.food.properties.filter(x => x !== p)
|
||||
},
|
||||
cancelAction: function () {
|
||||
this.$emit("hidden", "")
|
||||
},
|
||||
loadUnitConversions: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listUnitConversions(this.food.id).then(r => {
|
||||
this.unit_conversions = r.data
|
||||
})
|
||||
},
|
||||
addUnitConversion: function () {
|
||||
this.unit_conversions.push(
|
||||
{
|
||||
food: this.food,
|
||||
base_amount: 1,
|
||||
base_unit: null,
|
||||
converted_amount: 0,
|
||||
converted_unit: null,
|
||||
}
|
||||
)
|
||||
},
|
||||
deleteUnitConversion: function (uc) {
|
||||
this.unit_conversions = this.unit_conversions.filter(u => u !== uc)
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.destroyUnitConversion(uc.id).then(r => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="pl-1" v-if="recipe.last_cooked !== null">
|
||||
<span class="pl-1" v-if="recipe.last_cooked !== undefined && recipe.last_cooked !== null">
|
||||
<b-badge pill variant="primary" class="font-weight-normal"><i class="fas fa-utensils"></i> {{
|
||||
formatDate(recipe.last_cooked)
|
||||
}}</b-badge>
|
||||
|
||||
@@ -1,46 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-modal :id="'modal_' + id" @hidden="cancelAction">
|
||||
<template v-slot:modal-title>
|
||||
<h4 class="d-inline">{{ form.title }}</h4>
|
||||
<help-badge v-if="form.show_help" @show="show_help = true" @hide="show_help = false" :component="`GenericModal${form.title}`" />
|
||||
</template>
|
||||
<div v-for="(f, i) in form.fields" v-bind:key="i">
|
||||
<p v-if="visibleCondition(f, 'instruction')">{{ f.label }}</p>
|
||||
<lookup-input v-if="visibleCondition(f, 'lookup')" :form="f" :model="listModel(f.list)" @change="storeValue" :help="showHelp && f.help" />
|
||||
<checkbox-input class="mb-3" v-if="visibleCondition(f, 'checkbox')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" />
|
||||
<text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" />
|
||||
<choice-input v-if="visibleCondition(f, 'choice')" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" />
|
||||
<emoji-input v-if="visibleCondition(f, 'emoji')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
|
||||
<date-input v-if="visibleCondition(f, 'date')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" :subtitle="f.subtitle" />
|
||||
<number-input v-if="visibleCondition(f, 'number')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" />
|
||||
</div>
|
||||
<template v-slot:modal-footer>
|
||||
<div class="row w-100">
|
||||
<div class="col-6 align-self-end">
|
||||
<b-form-checkbox v-if="advancedForm" sm switch v-model="show_advanced">{{ $t("Advanced") }}</b-form-checkbox>
|
||||
</div>
|
||||
<div class="col-auto justify-content-end">
|
||||
<b-button class="mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
|
||||
<b-button class="mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||
</div>
|
||||
<template v-if="form_component !== undefined">
|
||||
<component :is="form_component" :id="'modal_' + id" :show="show" @hidden="cancelAction" :item1="item1"></component>
|
||||
</template>
|
||||
<template v-else>
|
||||
<b-modal :id="'modal_' + id" @hidden="cancelAction" size="lg">
|
||||
<template v-slot:modal-title>
|
||||
<h4 class="d-inline">{{ form.title }}</h4>
|
||||
<help-badge v-if="form.show_help" @show="show_help = true" @hide="show_help = false" :component="`GenericModal${form.title}`"/>
|
||||
</template>
|
||||
<div v-for="(f, i) in form.fields" v-bind:key="i">
|
||||
<p v-if="visibleCondition(f, 'instruction')">{{ f.label }}</p>
|
||||
<lookup-input v-if="visibleCondition(f, 'lookup')" :form="f" :model="listModel(f.list)" @change="storeValue" :help="showHelp && f.help"/>
|
||||
<checkbox-input class="mb-3" v-if="visibleCondition(f, 'checkbox')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help"/>
|
||||
<text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" :disabled="f.disabled"/>
|
||||
<choice-input v-if="visibleCondition(f, 'choice')" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder"/>
|
||||
<emoji-input v-if="visibleCondition(f, 'emoji')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue"/>
|
||||
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue"/>
|
||||
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value"/>
|
||||
<date-input v-if="visibleCondition(f, 'date')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" :subtitle="f.subtitle"/>
|
||||
<number-input v-if="visibleCondition(f, 'number')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle"/>
|
||||
</div>
|
||||
</template>
|
||||
</b-modal>
|
||||
<template v-slot:modal-footer>
|
||||
<div class="row w-100">
|
||||
<div class="col-6 align-self-end">
|
||||
<b-form-checkbox v-if="advancedForm" sm switch v-model="show_advanced">{{ $t("Advanced") }}</b-form-checkbox>
|
||||
</div>
|
||||
<div class="col-auto justify-content-end">
|
||||
<b-button class="mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
|
||||
<b-button class="mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import { getForm, formFunctions } from "@/utils/utils"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import {getForm, formFunctions} from "@/utils/utils"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import { ApiMixin, StandardToasts, ToastMixin, getUserPreference } from "@/utils/utils"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
import {ApiMixin, StandardToasts, ToastMixin, getUserPreference} from "@/utils/utils"
|
||||
import CheckboxInput from "@/components/Modals/CheckboxInput"
|
||||
import LookupInput from "@/components/Modals/LookupInput"
|
||||
import TextInput from "@/components/Modals/TextInput"
|
||||
@@ -54,10 +60,21 @@ import NumberInput from "@/components/Modals/NumberInput.vue";
|
||||
|
||||
export default {
|
||||
name: "GenericModalForm",
|
||||
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge,DateInput, NumberInput },
|
||||
components: {
|
||||
FileInput,
|
||||
CheckboxInput,
|
||||
LookupInput,
|
||||
TextInput,
|
||||
EmojiInput,
|
||||
ChoiceInput,
|
||||
SmallText,
|
||||
HelpBadge,
|
||||
DateInput,
|
||||
NumberInput
|
||||
},
|
||||
mixins: [ApiMixin, ToastMixin],
|
||||
props: {
|
||||
model: { required: true, type: Object },
|
||||
model: {required: true, type: Object},
|
||||
action: {
|
||||
type: Object,
|
||||
default() {
|
||||
@@ -76,7 +93,8 @@ export default {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
show: { required: true, type: Boolean, default: false },
|
||||
show: {required: true, type: Boolean, default: false},
|
||||
models: {required: false, type: Function, default: null}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -92,6 +110,10 @@ export default {
|
||||
mounted() {
|
||||
this.id = Math.random()
|
||||
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
|
||||
|
||||
if (this.models !== null) {
|
||||
this.Models = this.models // override models definition file with prop
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
advancedForm() {
|
||||
@@ -111,6 +133,15 @@ export default {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
form_component() {
|
||||
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
|
||||
// TODO this is not necessarily bad but maybe there are better options to do this
|
||||
if (this.form.component !== undefined) {
|
||||
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.form.component}`)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function () {
|
||||
@@ -153,6 +184,7 @@ export default {
|
||||
if (this.dirty) {
|
||||
this.dirty = false
|
||||
this.$emit("finish-action", "cancel")
|
||||
this.$emit("hidden")
|
||||
}
|
||||
},
|
||||
storeValue: function (field, value) {
|
||||
@@ -174,16 +206,16 @@ export default {
|
||||
return form
|
||||
},
|
||||
delete: function () {
|
||||
this.genericAPI(this.model, this.Actions.DELETE, { id: this.item1.id })
|
||||
this.genericAPI(this.model, this.Actions.DELETE, {id: this.item1.id})
|
||||
.then((result) => {
|
||||
this.$emit("finish-action")
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_DELETE)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status === 403){
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_DELETE_PROTECTED, err)
|
||||
}else {
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_DELETE, err)
|
||||
if (err.response.status === 403) {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE_PROTECTED, err)
|
||||
} else {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
}
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
@@ -193,22 +225,22 @@ export default {
|
||||
// if there is no item id assume it's a new item
|
||||
this.genericAPI(this.model, this.Actions.CREATE, this.form_data)
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { item: result.data })
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_CREATE)
|
||||
this.$emit("finish-action", {item: result.data})
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_CREATE)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.model, this.Actions.UPDATE, this.form_data)
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { item: result.data })
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_UPDATE)
|
||||
this.$emit("finish-action", {item: result.data})
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_UPDATE, err)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
}
|
||||
@@ -224,13 +256,13 @@ export default {
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.model, this.Actions.MOVE, { source: this.item1.id, target: this.form_data.target.id })
|
||||
this.genericAPI(this.model, this.Actions.MOVE, {source: this.item1.id, target: this.form_data.target.id})
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_MOVE)
|
||||
this.$emit("finish-action", {target: this.form_data.target.id})
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_MOVE)
|
||||
})
|
||||
.catch((err) => {
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_MOVE, err)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_MOVE, err)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
},
|
||||
@@ -250,11 +282,14 @@ export default {
|
||||
target: this.form_data.target.id,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { target: this.form_data.target.id, target_object: this.form_data.target }) //TODO temporary workaround to not change other apis
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_MERGE)
|
||||
this.$emit("finish-action", {
|
||||
target: this.form_data.target.id,
|
||||
target_object: this.form_data.target
|
||||
}) //TODO temporary workaround to not change other apis
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_MERGE)
|
||||
})
|
||||
.catch((err) => {
|
||||
StandardToasts.makeStandardToast(this,StandardToasts.FAIL_MERGE, err)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_MERGE, err)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group v-bind:label="label" class="mb-3">
|
||||
<b-form-input v-model="new_value" type="text" :placeholder="placeholder"></b-form-input>
|
||||
<b-form-input v-model="new_value" type="text" :placeholder="placeholder" :disabled="disabled"></b-form-input>
|
||||
<em v-if="help" class="small text-muted">{{ help }}</em>
|
||||
<small v-if="subtitle" class="text-muted">{{ subtitle }}</small>
|
||||
</b-form-group>
|
||||
@@ -18,6 +18,7 @@ export default {
|
||||
placeholder: { type: String, default: "You Should Add Placeholder Text" },
|
||||
help: { type: String, default: undefined },
|
||||
subtitle: { type: String, default: undefined },
|
||||
disabled: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
94
vue/src/components/OpenDataImportComponent.vue
Normal file
94
vue/src/components/OpenDataImportComponent.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<beta-warning></beta-warning>
|
||||
|
||||
<div v-if="metadata !== undefined">
|
||||
{{ $t('Data_Import_Info') }}
|
||||
<a href="https://github.com/TandoorRecipes/open-tandoor-data" target="_blank" rel="noreferrer nofollow">{{$t('Learn_More')}}</a>
|
||||
|
||||
|
||||
<select class="form-control" v-model="selected_version">
|
||||
<option v-for="v in metadata.versions" v-bind:key="v">{{ v }}</option>
|
||||
</select>
|
||||
|
||||
<b-checkbox v-model="update_existing" class="mt-1">{{ $t('Update_Existing_Data') }}</b-checkbox>
|
||||
<b-checkbox v-model="use_metric" class="mt-1">{{ $t('Use_Metric') }}</b-checkbox>
|
||||
|
||||
|
||||
<div v-if="selected_version !== undefined" class="mt-3">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{{ $t('Datatype') }}</th>
|
||||
<th>{{ $t('Number of Objects') }}</th>
|
||||
<th>{{ $t('Imported') }}</th>
|
||||
</tr>
|
||||
<tr v-for="d in metadata.datatypes" v-bind:key="d">
|
||||
<td>{{ $t(d.charAt(0).toUpperCase() + d.slice(1)) }}</td>
|
||||
<td>{{ metadata[selected_version][d] }}</td>
|
||||
<td>
|
||||
<template v-if="import_count !== undefined">{{ import_count[d] }}</template>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button class="btn btn-success" @click="doImport">{{ $t('Import') }}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import {ApiMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils";
|
||||
import axios from "axios";
|
||||
import BetaWarning from "@/components/BetaWarning.vue";
|
||||
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
|
||||
export default {
|
||||
name: "OpenDataImportComponent",
|
||||
mixins: [ApiMixin],
|
||||
components: {BetaWarning},
|
||||
data() {
|
||||
return {
|
||||
metadata: undefined,
|
||||
selected_version: undefined,
|
||||
update_existing: true,
|
||||
use_metric: true,
|
||||
import_count: undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
|
||||
axios.get(resolveDjangoUrl('api_import_open_data')).then(r => {
|
||||
this.metadata = r.data
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
doImport: function () {
|
||||
axios.post(resolveDjangoUrl('api_import_open_data'), {
|
||||
'selected_version': this.selected_version,
|
||||
'selected_datatypes': this.metadata.datatypes,
|
||||
'update_existing': this.update_existing,
|
||||
'use_metric': this.use_metric,
|
||||
}).then(r => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
|
||||
this.import_count = r.data
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
195
vue/src/components/PropertyViewComponent.vue
Normal file
195
vue/src/components/PropertyViewComponent.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
|
||||
<div class="card p-4 pb-2" v-if="recipe !== undefined">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<h5><i class="fas fa-database"></i> {{ $t('Properties') }}</h5>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<span v-if="!show_total">{{ $t('per_serving') }} </span>
|
||||
<span v-if="show_total">{{ $t('total') }} </span>
|
||||
|
||||
<a href="#" @click="show_total = !show_total">
|
||||
<i class="fas fa-toggle-on" v-if="!show_total"></i>
|
||||
<i class="fas fa-toggle-off" v-if="show_total"></i>
|
||||
</a>
|
||||
|
||||
<div v-if="hasRecipeProperties && hasFoodProperties">
|
||||
<span v-if="!show_recipe_properties">{{ $t('Food') }} </span>
|
||||
<span v-if="show_recipe_properties">{{ $t('Recipe') }} </span>
|
||||
|
||||
<a href="#" @click="show_recipe_properties = !show_recipe_properties">
|
||||
<i class="fas fa-toggle-on" v-if="!show_recipe_properties"></i>
|
||||
<i class="fas fa-toggle-off" v-if="show_recipe_properties"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
|
||||
<table class="table table-bordered table-sm">
|
||||
|
||||
<tr v-for="p in property_list" v-bind:key="`id_${p.id}`">
|
||||
<td>
|
||||
|
||||
{{ p.icon }} {{ p.name }}
|
||||
</td>
|
||||
<td class="text-right">{{ get_amount(p.property_amount) }}</td>
|
||||
<td class=""> {{ p.unit }}</td>
|
||||
|
||||
<td class="align-middle text-center" v-if="!show_recipe_properties">
|
||||
<a href="#" @click="selected_property = p">
|
||||
<i v-if="p.missing_value" class="text-warning fas fa-exclamation-triangle"></i>
|
||||
<i v-if="!p.missing_value" class="text-muted fas fa-info-circle"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<b-modal id="id_modal_property_overview" :title="selected_property.name" v-model="show_modal" v-if="selected_property !== undefined"
|
||||
@hidden="selected_property = undefined">
|
||||
<template v-if="selected_property !== undefined">
|
||||
{{ selected_property.description }}
|
||||
<table class="table table-bordered">
|
||||
<tr v-for="f in selected_property.food_values"
|
||||
v-bind:key="`id_${selected_property.id}_food_${f.id}`">
|
||||
<td><a href="#" @click="openFoodEditModal(f)">{{ f.food }}</a></td>
|
||||
<td>{{ f.value }} {{ selected_property.unit }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
</b-modal>
|
||||
|
||||
<generic-modal-form
|
||||
:model="Models.FOOD"
|
||||
:action="Actions.UPDATE"
|
||||
:item1="selected_food"
|
||||
:show="show_food_edit_modal"
|
||||
@hidden="foodEditorHidden"
|
||||
>
|
||||
</generic-modal-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiMixin, StandardToasts} from "@/utils/utils";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
|
||||
export default {
|
||||
name: "PropertyViewComponent",
|
||||
mixins: [ApiMixin],
|
||||
components: {GenericModalForm},
|
||||
props: {
|
||||
recipe: Object,
|
||||
servings: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected_property: undefined,
|
||||
selected_food: undefined,
|
||||
show_food_edit_modal: false,
|
||||
show_total: false,
|
||||
show_recipe_properties: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show_modal: function () {
|
||||
return this.selected_property !== undefined
|
||||
},
|
||||
hasRecipeProperties: function () {
|
||||
return this.recipe.properties.length !== 0
|
||||
},
|
||||
hasFoodProperties: function () {
|
||||
let has_food_properties = false
|
||||
for (const [key, fp] of Object.entries(this.recipe.food_properties)) {
|
||||
if (fp.total_value !== 0) {
|
||||
has_food_properties = true
|
||||
}
|
||||
}
|
||||
return has_food_properties
|
||||
},
|
||||
property_list: function () {
|
||||
let pt_list = []
|
||||
if (this.show_recipe_properties) {
|
||||
this.recipe.properties.forEach(rp => {
|
||||
pt_list.push(
|
||||
{
|
||||
'id': rp.property_type.id,
|
||||
'name': rp.property_type.name,
|
||||
'description': rp.property_type.description,
|
||||
'icon': rp.property_type.icon,
|
||||
'food_values': [],
|
||||
'property_amount': rp.property_amount,
|
||||
'missing_value': false,
|
||||
'unit': rp.property_type.unit,
|
||||
}
|
||||
)
|
||||
})
|
||||
} else {
|
||||
for (const [key, fp] of Object.entries(this.recipe.food_properties)) {
|
||||
pt_list.push(
|
||||
{
|
||||
'id': fp.id,
|
||||
'name': fp.name,
|
||||
'description': fp.description,
|
||||
'icon': fp.icon,
|
||||
'food_values': fp.food_values,
|
||||
'property_amount': fp.total_value,
|
||||
'missing_value': fp.missing_value,
|
||||
'unit': fp.unit,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
return pt_list
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.hasRecipeProperties && !this.hasFoodProperties) {
|
||||
this.show_recipe_properties = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
get_amount: function (amount) {
|
||||
if (this.show_total) {
|
||||
return (amount * (this.servings / this.recipe.servings)).toLocaleString(window.CUSTOM_LOCALE, {
|
||||
'maximumFractionDigits': 2,
|
||||
'minimumFractionDigits': 2
|
||||
})
|
||||
} else {
|
||||
return (amount / this.recipe.servings).toLocaleString(window.CUSTOM_LOCALE, {
|
||||
'maximumFractionDigits': 2,
|
||||
'minimumFractionDigits': 2
|
||||
})
|
||||
}
|
||||
},
|
||||
openFoodEditModal: function (food) {
|
||||
console.log(food)
|
||||
let apiClient = ApiApiFactory()
|
||||
apiClient.retrieveFood(food.id).then(r => {
|
||||
this.selected_food = r.data;
|
||||
this.show_food_edit_modal = true
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
},
|
||||
foodEditorHidden: function () {
|
||||
this.show_food_edit_modal = false;
|
||||
this.$emit("foodUpdated", "")
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -35,12 +35,12 @@
|
||||
|
||||
<div class="card-img-overlay d-flex flex-column justify-content-left float-left text-left pt-2" style="width:40%"
|
||||
v-if="recipe.working_time !== 0 || recipe.waiting_time !== 0">
|
||||
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0">
|
||||
<b-badge pill variant="light" class="mt-1 font-weight-normal" v-if="recipe.working_time !== 0 && recipe.working_time !== undefined">
|
||||
<i
|
||||
class="fa fa-clock"></i> {{ working_time }}
|
||||
</b-badge>
|
||||
<b-badge pill variant="secondary" class="mt-1 font-weight-normal"
|
||||
v-if="recipe.waiting_time !== 0">
|
||||
v-if="recipe.waiting_time !== 0 && recipe.waiting_time !== undefined">
|
||||
<i class="fa fa-pause"></i> {{ waiting_time }}
|
||||
</b-badge>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="justify-content-end">
|
||||
<recipe-context-menu :recipe="recipe" class="justify-content-end float-right align-items-end pr-0"
|
||||
:disabled_options="context_disabled_options"
|
||||
v-if="recipe !== null"></recipe-context-menu>
|
||||
v-if="recipe !== null && show_context_menu"></recipe-context-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<b-badge pill variant="info" v-if="!recipe.internal">{{ $t("External") }}</b-badge>
|
||||
<b-badge pill variant="info" v-if="recipe.internal !== undefined && !recipe.internal">{{ $t("External") }}</b-badge>
|
||||
</template>
|
||||
|
||||
</b-card-text>
|
||||
|
||||
351
vue/src/components/RecipeViewComponent.vue
Normal file
351
vue/src/components/RecipeViewComponent.vue
Normal file
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<template v-if="loading">
|
||||
<loading-spinner></loading-spinner>
|
||||
</template>
|
||||
|
||||
<div v-if="!loading" style="padding-bottom: 60px">
|
||||
<RecipeSwitcher ref="ref_recipe_switcher" @switch="quickSwitch($event)"/>
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{{ recipe.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-center">
|
||||
<div class="col col-md-12">
|
||||
<recipe-rating :recipe="recipe"></recipe-rating>
|
||||
<last-cooked :recipe="recipe" class="mt-2"></last-cooked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-auto">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<i>{{ recipe.description }}</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<keywords-component :recipe="recipe"></keywords-component>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<div class="row align-items-center">
|
||||
<div class="col col-md-3">
|
||||
<div class="d-flex">
|
||||
<div class="my-auto mr-1">
|
||||
<i class="fas fa-fw fa-user-clock fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div class="my-auto mr-1">
|
||||
<span class="text-primary"><b>{{ $t("Preparation") }}</b></span><br/>
|
||||
{{ working_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-3">
|
||||
<div class="row d-flex">
|
||||
<div class="my-auto mr-1">
|
||||
<i class="far fa-fw fa-clock fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div class="my-auto mr-1">
|
||||
<span class="text-primary"><b>{{ $t("Waiting") }}</b></span><br/>
|
||||
{{ waiting_time }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-4 col-10 mt-2 mt-md-0">
|
||||
<div class="d-flex">
|
||||
<div class="my-auto mr-1">
|
||||
<i class="fas fa-fw fa-pizza-slice fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div class="my-auto mr-1">
|
||||
<CustomInputSpinButton v-model.number="servings"/>
|
||||
</div>
|
||||
<div class="my-auto mr-1">
|
||||
<span class="text-primary">
|
||||
<b>
|
||||
<template v-if="recipe.servings_text === ''">{{ $t("Servings") }}</template>
|
||||
<template v-else>{{ recipe.servings_text }}</template>
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-2 col-2 mt-2 mt-md-0 text-right">
|
||||
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"
|
||||
:disabled_options="{print:false}"></recipe-context-menu>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2"
|
||||
v-if="recipe && ingredient_count > 0 && (recipe.show_ingredient_overview || recipe.steps.length < 2)">
|
||||
<ingredients-card
|
||||
:recipe="recipe.id"
|
||||
:steps="recipe.steps"
|
||||
:ingredient_factor="ingredient_factor"
|
||||
:servings="servings"
|
||||
:header="true"
|
||||
id="ingredient_container"
|
||||
@checked-state-changed="updateIngredientCheckedState"
|
||||
@change-servings="servings = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img class="img img-fluid rounded" :src="recipe.image" :alt="$t('Recipe_Image')"
|
||||
v-if="recipe.image !== null" @load="onImgLoad"
|
||||
:style="{ 'max-height': ingredient_height }"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!recipe.internal">
|
||||
<div v-if="recipe.file_path.includes('.pdf')">
|
||||
<PdfViewer :recipe="recipe"></PdfViewer>
|
||||
</div>
|
||||
<div
|
||||
v-if="recipe.file_path.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
|
||||
<ImageViewer :recipe="recipe"></ImageViewer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
|
||||
<step-component
|
||||
:recipe="recipe"
|
||||
:step="s"
|
||||
:ingredient_factor="ingredient_factor"
|
||||
:index="index"
|
||||
:start_time="start_time"
|
||||
@update-start-time="updateStartTime"
|
||||
@checked-state-changed="updateIngredientCheckedState"
|
||||
></step-component>
|
||||
</div>
|
||||
|
||||
<div v-if="recipe.source_url !== null">
|
||||
<h6 class="d-print-none"><i class="fas fa-file-import"></i> {{ $t("Imported_From") }}</h6>
|
||||
<span class="text-muted mt-1"><a style="overflow-wrap: break-word;"
|
||||
:href="recipe.source_url">{{ recipe.source_url }}</a></span>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh; ">
|
||||
<div class="col-lg-6 offset-lg-3 col-12">
|
||||
<property-view-component :recipe="recipe" :servings="servings" @foodUpdated="loadRecipe(recipe.id)"></property-view-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
|
||||
|
||||
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh"
|
||||
v-if="share_uid !== 'None' && !loading">
|
||||
<div class="col col-md-12">
|
||||
<import-tandoor></import-tandoor> <br/>
|
||||
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" class="mt-3">{{ $t("Report Abuse") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import {apiLoadRecipe} from "@/utils/api"
|
||||
|
||||
import RecipeContextMenu from "@/components/RecipeContextMenu"
|
||||
import {ResolveUrlMixin, ToastMixin, calculateHourMinuteSplit} from "@/utils/utils"
|
||||
|
||||
import PdfViewer from "@/components/PdfViewer"
|
||||
import ImageViewer from "@/components/ImageViewer"
|
||||
|
||||
import moment from "moment"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
|
||||
import RecipeRating from "@/components/RecipeRating"
|
||||
import LastCooked from "@/components/LastCooked"
|
||||
import IngredientsCard from "@/components/IngredientsCard"
|
||||
import StepComponent from "@/components/StepComponent"
|
||||
import KeywordsComponent from "@/components/KeywordsComponent"
|
||||
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
|
||||
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import ImportTandoor from "@/components/Modals/ImportTandoor.vue";
|
||||
import PropertyViewComponent from "@/components/PropertyViewComponent.vue";
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: "RecipeView",
|
||||
mixins: [ResolveUrlMixin, ToastMixin],
|
||||
components: {
|
||||
ImportTandoor,
|
||||
LastCooked,
|
||||
RecipeRating,
|
||||
PdfViewer,
|
||||
ImageViewer,
|
||||
IngredientsCard,
|
||||
StepComponent,
|
||||
RecipeContextMenu,
|
||||
KeywordsComponent,
|
||||
LoadingSpinner,
|
||||
AddRecipeToBook,
|
||||
RecipeSwitcher,
|
||||
CustomInputSpinButton,
|
||||
PropertyViewComponent,
|
||||
},
|
||||
computed: {
|
||||
ingredient_factor: function () {
|
||||
return this.servings / this.recipe.servings
|
||||
},
|
||||
ingredient_count() {
|
||||
return this.recipe?.steps.map((x) => x.ingredients).flat().length
|
||||
},
|
||||
working_time: function () {
|
||||
return calculateHourMinuteSplit(this.recipe.working_time)
|
||||
},
|
||||
waiting_time: function () {
|
||||
return calculateHourMinuteSplit(this.recipe.waiting_time)
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
recipe: undefined,
|
||||
rootrecipe: undefined,
|
||||
servings: 1,
|
||||
servings_cache: {},
|
||||
start_time: "",
|
||||
share_uid: window.SHARE_UID,
|
||||
wake_lock: null,
|
||||
ingredient_height: '250',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
servings(newVal, oldVal) {
|
||||
this.servings_cache[this.recipe.id] = this.servings
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadRecipe(window.RECIPE_ID)
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
this.requestWakeLock()
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.destroyWakeLock()
|
||||
},
|
||||
methods: {
|
||||
requestWakeLock: async function () {
|
||||
if ('wakeLock' in navigator) {
|
||||
try {
|
||||
this.wake_lock = await navigator.wakeLock.request('screen')
|
||||
document.addEventListener('visibilitychange', this.visibilityChange)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleResize: function () {
|
||||
if (document.getElementById('nutrition_container') !== null) {
|
||||
this.ingredient_height = document.getElementById('ingredient_container').clientHeight - document.getElementById('nutrition_container').clientHeight
|
||||
} else {
|
||||
this.ingredient_height = document.getElementById('ingredient_container').clientHeight
|
||||
}
|
||||
},
|
||||
destroyWakeLock: function () {
|
||||
if (this.wake_lock != null) {
|
||||
this.wake_lock.release()
|
||||
.then(() => {
|
||||
this.wake_lock = null
|
||||
});
|
||||
}
|
||||
|
||||
document.removeEventListener('visibilitychange', this.visibilityChange)
|
||||
},
|
||||
visibilityChange: async function () {
|
||||
if (this.wake_lock != null && document.visibilityState === 'visible') {
|
||||
await this.requestWakeLock()
|
||||
}
|
||||
},
|
||||
loadRecipe: function (recipe_id) {
|
||||
apiLoadRecipe(recipe_id).then((recipe) => {
|
||||
let total_time = 0
|
||||
for (let step of recipe.steps) {
|
||||
for (let ingredient of step.ingredients) {
|
||||
this.$set(ingredient, "checked", false)
|
||||
}
|
||||
|
||||
step.time_offset = total_time
|
||||
total_time += step.time
|
||||
}
|
||||
|
||||
// set start time only if there are any steps with timers (otherwise no timers are rendered)
|
||||
if (total_time > 0) {
|
||||
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
|
||||
}
|
||||
|
||||
|
||||
if (recipe.image === null) this.printReady()
|
||||
|
||||
this.recipe = this.rootrecipe = recipe
|
||||
this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings
|
||||
this.loading = false
|
||||
|
||||
setTimeout(() => {
|
||||
this.handleResize()
|
||||
}, 100)
|
||||
})
|
||||
},
|
||||
updateStartTime: function (e) {
|
||||
this.start_time = e
|
||||
},
|
||||
updateIngredientCheckedState: function (e) {
|
||||
for (let step of this.recipe.steps) {
|
||||
for (let ingredient of step.ingredients) {
|
||||
if (ingredient.id === e.id) {
|
||||
this.$set(ingredient, "checked", !ingredient.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
quickSwitch: function (e) {
|
||||
if (e === -1) {
|
||||
this.recipe = this.rootrecipe
|
||||
this.servings = this.servings_cache[this.rootrecipe?.id ?? 1]
|
||||
} else {
|
||||
this.recipe = e
|
||||
this.servings = this.servings_cache?.[e.id] ?? e.servings
|
||||
}
|
||||
},
|
||||
printReady: function () {
|
||||
const template = document.createElement("template");
|
||||
template.id = "printReady";
|
||||
document.body.appendChild(template);
|
||||
},
|
||||
onImgLoad: function () {
|
||||
this.printReady()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app > div > div {
|
||||
break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user