mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 12:18:45 -05:00
basic property viewer
This commit is contained in:
@@ -40,7 +40,7 @@ const routes = [
|
|||||||
{path: '/view/recipe/:id', redirect: {name: 'RecipeViewPage'}}, // old Tandoor v1 url pattern
|
{path: '/view/recipe/:id', redirect: {name: 'RecipeViewPage'}}, // old Tandoor v1 url pattern
|
||||||
|
|
||||||
{path: '/list/:model?', component: () => import("@/pages/ModelListPage.vue"), props: true, name: 'ModelListPage'},
|
{path: '/list/:model?', component: () => import("@/pages/ModelListPage.vue"), props: true, name: 'ModelListPage'},
|
||||||
{path: '/edit/:model/:id?', component: () => import("@/pages/ModelListPage.vue"), props: true, name: 'ModelEditPage'},
|
{path: '/edit/:model/:id?', component: () => import("@/pages/ModelEditPage.vue"), props: true, name: 'ModelEditPage'},
|
||||||
|
|
||||||
{path: '/ingredient-editor', component: () => import("@/pages/IngredientEditorPage.vue"), name: 'IngredientEditorPage'},
|
{path: '/ingredient-editor', component: () => import("@/pages/IngredientEditorPage.vue"), name: 'IngredientEditorPage'},
|
||||||
{path: '/property-editor', component: () => import("@/pages/PropertyEditorPage.vue"), name: 'PropertyEditorPage'},
|
{path: '/property-editor', component: () => import("@/pages/PropertyEditorPage.vue"), name: 'PropertyEditorPage'},
|
||||||
|
|||||||
@@ -1,21 +1,99 @@
|
|||||||
<template>
|
<template>
|
||||||
{{hasFoodProperties}}
|
<v-card class="mt-2">
|
||||||
{{hasRecipeProperties}}
|
<v-card-title>
|
||||||
|
<v-icon icon="$properties"></v-icon>
|
||||||
|
{{ $t('Properties') }}
|
||||||
|
|
||||||
|
<v-btn-toggle border divided density="compact" class="float-right" v-if="hasRecipeProperties && hasRecipeProperties" v-model="sourceSelectedToShow">
|
||||||
|
<v-btn size="small" value="food">{{ $t('Food') }}</v-btn>
|
||||||
|
<v-btn size="small" value="recipe">{{ $t('Recipe') }}</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-table density="compact" style="max-width: 800px">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>{{ $t('per_serving') }}</th>
|
||||||
|
<th>{{ $t('total') }}</th>
|
||||||
|
<th v-if="sourceSelectedToShow == 'food'"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="p in propertyList" :key="p.id">
|
||||||
|
<td>{{p.name}}</td>
|
||||||
|
<td>{{$n(p.propertyAmountPerServing)}} {{p.unit}}</td>
|
||||||
|
<td>{{$n(p.propertyAmountTotal)}} {{p.unit}}</td>
|
||||||
|
<td v-if="sourceSelectedToShow == 'food'">
|
||||||
|
<v-btn @click="dialogProperty = p; dialog = true" variant="plain" color="warning" icon="fa-solid fa-triangle-exclamation" size="small" v-if="p.missingValue"></v-btn>
|
||||||
|
<v-btn @click="dialogProperty = p; dialog = true" variant="plain" icon="fa-solid fa-circle-info" size="small" v-if="!p.missingValue"></v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-dialog max-width="900px" v-model="dialog">
|
||||||
|
<v-card v-if="dialogProperty">
|
||||||
|
<v-closable-card-title :title="`${dialogProperty.propertyAmountTotal} ${dialogProperty.unit} ${dialogProperty.name}`" :sub-title="$t('total')" icon="$properties" v-model="dialog"></v-closable-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item border v-for="fv in dialogProperty.foodValues">
|
||||||
|
<template #prepend>
|
||||||
|
<v-progress-circular size="55" width="5" :model-value="(fv.value/dialogProperty.propertyAmountTotal)*100" :color="colorScale((fv.value/dialogProperty.propertyAmountTotal)*100)" v-if="fv.value != null">{{Math.round((fv.value/dialogProperty.propertyAmountTotal)*100)}}%</v-progress-circular>
|
||||||
|
<v-progress-circular size="55" width="5" v-if="fv.value == null">?</v-progress-circular>
|
||||||
|
</template>
|
||||||
|
<span class="ms-2">
|
||||||
|
{{ fv.food }}
|
||||||
|
</span>
|
||||||
|
<template #append>
|
||||||
|
<v-chip v-if="fv.value">{{$n(fv.value)}} {{dialogProperty.unit}}</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn :to="{ name: 'PropertyEditorPage', query: {recipe: recipe.id} }">{{$t('Property_Editor')}}</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {computed, PropType} from "vue";
|
import {computed, onMounted, PropType, ref} from "vue";
|
||||||
import {Recipe} from "@/openapi";
|
import {PropertyType, Recipe} from "@/openapi";
|
||||||
|
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||||
|
|
||||||
|
type PropertyWrapper = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
description?: string,
|
||||||
|
foodValues: [],
|
||||||
|
propertyAmountPerServing: number,
|
||||||
|
propertyAmountTotal: number,
|
||||||
|
missingValue: boolean,
|
||||||
|
unit?: string,
|
||||||
|
type: PropertyType,
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
recipe: {type: {} as PropType<Recipe>, required: true}
|
recipe: {type: {} as PropType<Recipe>, required: true},
|
||||||
|
servings: { type: Number, required: true, },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determines if the recipe has properties on the recipe directly
|
||||||
|
*/
|
||||||
const hasRecipeProperties = computed(() => {
|
const hasRecipeProperties = computed(() => {
|
||||||
return props.recipe.properties != undefined && props.recipe.properties.length > 0
|
return props.recipe.properties != undefined && props.recipe.properties.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* determines if the recipe has calculated properties based on its foods
|
||||||
|
*/
|
||||||
const hasFoodProperties = computed(() => {
|
const hasFoodProperties = computed(() => {
|
||||||
let propertiesFound = false
|
let propertiesFound = false
|
||||||
for (const [key, fp] of Object.entries(props.recipe.foodProperties)) {
|
for (const [key, fp] of Object.entries(props.recipe.foodProperties)) {
|
||||||
@@ -26,6 +104,88 @@ const hasFoodProperties = computed(() => {
|
|||||||
return propertiesFound
|
return propertiesFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* compute list of properties based on recipe or food, depending on what is selected
|
||||||
|
*/
|
||||||
|
const propertyList = computed(() => {
|
||||||
|
let ptList = [] as PropertyWrapper[]
|
||||||
|
if (sourceSelectedToShow.value == 'recipe') {
|
||||||
|
if (hasRecipeProperties.value) {
|
||||||
|
props.recipe.properties.forEach(rp => {
|
||||||
|
ptList.push(
|
||||||
|
{
|
||||||
|
id: rp.propertyType.id!,
|
||||||
|
name: rp.propertyType.name,
|
||||||
|
description: rp.propertyType.description,
|
||||||
|
foodValues: [],
|
||||||
|
propertyAmountPerServing: rp.propertyAmount,
|
||||||
|
propertyAmountTotal: rp.propertyAmount * props.recipe.servings * (props.servings / props.recipe.servings),
|
||||||
|
missingValue: false,
|
||||||
|
unit: rp.propertyType.unit,
|
||||||
|
type: rp.propertyType,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, fp] of Object.entries(props.recipe.foodProperties)) {
|
||||||
|
ptList.push(
|
||||||
|
{
|
||||||
|
id: fp.id,
|
||||||
|
name: fp.name,
|
||||||
|
description: fp.description,
|
||||||
|
icon: fp.icon,
|
||||||
|
foodValues: fp.food_values,
|
||||||
|
propertyAmountPerServing: fp.total_value / props.recipe.servings,
|
||||||
|
propertyAmountTotal: fp.total_value * (props.servings / props.recipe.servings),
|
||||||
|
missingValue: fp.missing_value,
|
||||||
|
unit: fp.unit,
|
||||||
|
type: fp,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(a, b) {
|
||||||
|
if (a.type.order > b.type.order) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (a.type.order < b.type.order) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return ptList.sort(compare)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceSelectedToShow = ref<'recipe' | 'food'>("food")
|
||||||
|
const dialog = ref(false)
|
||||||
|
const dialogProperty = ref<undefined|PropertyWrapper>(undefined)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!hasFoodProperties) {
|
||||||
|
sourceSelectedToShow.value = "recipe"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return a color based on the given number
|
||||||
|
* used to color the percentage of each food contributing to the total value of a property
|
||||||
|
* @param percentage
|
||||||
|
*/
|
||||||
|
function colorScale(percentage: number){
|
||||||
|
if(percentage > 80){
|
||||||
|
return 'error'
|
||||||
|
}
|
||||||
|
if(percentage > 50){
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
if(percentage > 30){
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,13 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<property-view :recipe="recipe" :servings="servings"></property-view>
|
||||||
|
|
||||||
<v-expansion-panels class="mt-2">
|
<v-expansion-panels class="mt-2">
|
||||||
<v-expansion-panel>
|
<v-expansion-panel>
|
||||||
<v-expansion-panel-title><v-icon icon="$properties" class="me-2"></v-icon> {{ $t('Properties') }}</v-expansion-panel-title>
|
<v-expansion-panel-title><v-icon icon="$properties" class="me-2"></v-icon> {{ $t('Properties') }}</v-expansion-panel-title>
|
||||||
<v-expansion-panel-text>
|
<v-expansion-panel-text>
|
||||||
<property-view :recipe="recipe"></property-view>
|
<property-view :recipe="recipe" :ingredient-factor="ingredientFactor"></property-view>
|
||||||
</v-expansion-panel-text>
|
</v-expansion-panel-text>
|
||||||
</v-expansion-panel>
|
</v-expansion-panel>
|
||||||
|
|
||||||
@@ -144,7 +146,6 @@ import RecipeImage from "@/components/display/RecipeImage.vue";
|
|||||||
import ExternalRecipeViewer from "@/components/display/ExternalRecipeViewer.vue";
|
import ExternalRecipeViewer from "@/components/display/ExternalRecipeViewer.vue";
|
||||||
import {useWakeLock} from "@vueuse/core";
|
import {useWakeLock} from "@vueuse/core";
|
||||||
import StepView from "@/components/display/StepView.vue";
|
import StepView from "@/components/display/StepView.vue";
|
||||||
import IngredientsTable from "@/components/display/IngredientsTable.vue";
|
|
||||||
import {DateTime} from "luxon";
|
import {DateTime} from "luxon";
|
||||||
import PropertyView from "@/components/display/PropertyView.vue";
|
import PropertyView from "@/components/display/PropertyView.vue";
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
<v-tabs-window-item value="properties">
|
<v-tabs-window-item value="properties">
|
||||||
<v-form :disabled="loading || fileApiLoading">
|
<v-form :disabled="loading || fileApiLoading">
|
||||||
<v-alert class="mb-2" icon="$help">{{ $t('PropertiesFoodHelp') }}</v-alert>
|
<closable-help-alert :text="$t('PropertiesFoodHelp')"></closable-help-alert>
|
||||||
<properties-editor v-model="editingObj.properties" :amount-for="$t('Serving')"></properties-editor>
|
<properties-editor v-model="editingObj.properties" :amount-for="$t('Serving')"></properties-editor>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
|
|||||||
@@ -331,7 +331,7 @@
|
|||||||
"Private_Recipe_Help": "Dieses Rezept ist nur für dich und Personen mit denen du es geteilt hast sichtbar.",
|
"Private_Recipe_Help": "Dieses Rezept ist nur für dich und Personen mit denen du es geteilt hast sichtbar.",
|
||||||
"Profile": "Profil",
|
"Profile": "Profil",
|
||||||
"Properties": "Eigenschaften",
|
"Properties": "Eigenschaften",
|
||||||
"PropertiesFoodHelp": "Eigenschaften können für Rezepte und Lebensmittel erfasst werden. Eigenschaften von Lebensmitteln werden entsprechend der Menge für das Rezept ausgerechnet und überschreiben die Rezepteigenschaften. ",
|
"PropertiesFoodHelp": "Eigenschaften können für Rezepte und Lebensmittel erfasst werden. Eigenschaften von Lebensmitteln werden automatisch entsprechend der im Rezept enthaltenen Menge berechnet. ",
|
||||||
"Properties_Food_Amount": "Eigenschaften: Lebensmittelmenge",
|
"Properties_Food_Amount": "Eigenschaften: Lebensmittelmenge",
|
||||||
"Properties_Food_Unit": "Eigenschaft Einheit",
|
"Properties_Food_Unit": "Eigenschaft Einheit",
|
||||||
"Property": "Eigenschaft",
|
"Property": "Eigenschaft",
|
||||||
|
|||||||
@@ -329,7 +329,7 @@
|
|||||||
"Private_Recipe_Help": "Recipe is only shown to you and people its shared with.",
|
"Private_Recipe_Help": "Recipe is only shown to you and people its shared with.",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Properties": "Properties",
|
"Properties": "Properties",
|
||||||
"PropertiesFoodHelp": "Properties can be added to Recipes and Foods. Properties on Foods are calculated according based on their amount in the recipe and override the recipe properties.",
|
"PropertiesFoodHelp": "Properties can be added to Recipes and Foods. Properties on Foods are automatically calculated based on their amount in the recipe.",
|
||||||
"Properties_Food_Amount": "Properties Food Amount",
|
"Properties_Food_Amount": "Properties Food Amount",
|
||||||
"Properties_Food_Unit": "Properties Food Unit",
|
"Properties_Food_Unit": "Properties Food Unit",
|
||||||
"Property": "Property",
|
"Property": "Property",
|
||||||
|
|||||||
Reference in New Issue
Block a user