basic property viewer

This commit is contained in:
vabene1111
2025-03-21 20:28:26 +01:00
parent 05472b5a29
commit fbab90e954
6 changed files with 172 additions and 11 deletions

View File

@@ -40,7 +40,7 @@ const routes = [
{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: '/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: '/property-editor', component: () => import("@/pages/PropertyEditorPage.vue"), name: 'PropertyEditorPage'},

View File

@@ -1,21 +1,99 @@
<template>
{{hasFoodProperties}}
{{hasRecipeProperties}}
<v-card class="mt-2">
<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>
<script setup lang="ts">
import {computed, PropType} from "vue";
import {Recipe} from "@/openapi";
import {computed, onMounted, PropType, ref} from "vue";
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({
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(() => {
return props.recipe.properties != undefined && props.recipe.properties.length > 0
})
/**
* determines if the recipe has calculated properties based on its foods
*/
const hasFoodProperties = computed(() => {
let propertiesFound = false
for (const [key, fp] of Object.entries(props.recipe.foodProperties)) {
@@ -26,6 +104,88 @@ const hasFoodProperties = computed(() => {
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>

View File

@@ -72,11 +72,13 @@
</v-card>
</template>
<property-view :recipe="recipe" :servings="servings"></property-view>
<v-expansion-panels class="mt-2">
<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-text>
<property-view :recipe="recipe"></property-view>
<property-view :recipe="recipe" :ingredient-factor="ingredientFactor"></property-view>
</v-expansion-panel-text>
</v-expansion-panel>
@@ -144,7 +146,6 @@ import RecipeImage from "@/components/display/RecipeImage.vue";
import ExternalRecipeViewer from "@/components/display/ExternalRecipeViewer.vue";
import {useWakeLock} from "@vueuse/core";
import StepView from "@/components/display/StepView.vue";
import IngredientsTable from "@/components/display/IngredientsTable.vue";
import {DateTime} from "luxon";
import PropertyView from "@/components/display/PropertyView.vue";

View File

@@ -89,7 +89,7 @@
</v-tabs-window-item>
<v-tabs-window-item value="properties">
<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>
</v-form>
</v-tabs-window-item>

View File

@@ -331,7 +331,7 @@
"Private_Recipe_Help": "Dieses Rezept ist nur für dich und Personen mit denen du es geteilt hast sichtbar.",
"Profile": "Profil",
"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_Unit": "Eigenschaft Einheit",
"Property": "Eigenschaft",

View File

@@ -329,7 +329,7 @@
"Private_Recipe_Help": "Recipe is only shown to you and people its shared with.",
"Profile": "Profile",
"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_Unit": "Properties Food Unit",
"Property": "Property",