mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-06 22:58:19 -05:00
Basic ingredient editor
This commit is contained in:
@@ -27,11 +27,13 @@ import ApiSettings from "@/components/settings/ApiSettings.vue";
|
||||
import ModelListPage from "@/pages/ModelListPage.vue";
|
||||
import ModelEditPage from "@/pages/ModelEditPage.vue";
|
||||
import RecipeImportPage from "@/pages/RecipeImportPage.vue";
|
||||
import IngredientEditorPage from "@/pages/IngredientEditorPage.vue";
|
||||
|
||||
const routes = [
|
||||
{path: '/', component: StartPage, name: 'view_home'},
|
||||
{path: '/test', component: TestPage, name: 'view_test'},
|
||||
{path: '/settings', component: SettingsPage, name: 'view_settings', redirect: '/settings/account',
|
||||
{
|
||||
path: '/settings', component: SettingsPage, name: 'view_settings', redirect: '/settings/account',
|
||||
children: [
|
||||
{path: 'account', component: AccountSettings, name: 'view_settings_account'},
|
||||
{path: 'cosmetic', component: CosmeticSettings, name: 'view_settings_cosmetic'},
|
||||
@@ -41,7 +43,8 @@ const routes = [
|
||||
{path: 'space-members', component: SpaceMemberSettings, name: 'view_settings_space_member'},
|
||||
{path: 'user-space', component: UserSpaceSettings, name: 'view_settings_user_space'},
|
||||
{path: 'api', component: ApiSettings, name: 'view_settings_api'},
|
||||
]},
|
||||
]
|
||||
},
|
||||
//{path: '/settings/:page', component: SettingsPage, name: 'view_settings_page', props: true},
|
||||
{path: '/search', component: SearchPage, name: 'view_search'},
|
||||
{path: '/shopping', component: ShoppingListPage, name: 'view_shopping'},
|
||||
@@ -51,9 +54,10 @@ const routes = [
|
||||
{path: '/recipe/:id', component: RecipeViewPage, name: 'view_recipe', props: true},
|
||||
{path: '/recipe/edit/:recipe_id', component: RecipeEditPage, name: 'edit_recipe', props: true},
|
||||
|
||||
{path: '/list/:model?', component: ModelListPage, props: true, name: 'ModelListPage'},
|
||||
{path: '/edit/:model/:id?', component: ModelEditPage, props: true, name: 'ModelEditPage'},
|
||||
{path: '/list/:model?', component: ModelListPage, props: true, name: 'ModelListPage'},
|
||||
{path: '/edit/:model/:id?', component: ModelEditPage, props: true, name: 'ModelEditPage'},
|
||||
|
||||
{path: '/ingredient-editor', component: IngredientEditorPage, name: 'IngredientEditorPage'},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<!-- TODO label is not showing for some reason, for now in placeholder -->
|
||||
<!-- TODO support density prop -->
|
||||
<v-input :hint="props.hint" persistent-hint :label="props.label" class="" >
|
||||
<v-input :hint="props.hint" persistent-hint :label="props.label" >
|
||||
|
||||
<!-- TODO resolve-on-load false for now, race condition with model class, make prop once better solution is found -->
|
||||
<Multiselect
|
||||
|
||||
:ref="`ref_${props.id}`"
|
||||
class="material-multiselect"
|
||||
class="material-multiselect "
|
||||
:class="{'model-select--density-compact': props.density == 'compact', 'model-select--density-comfortable': props.density == 'comfortable', 'model-select--density-default': props.density == ''}"
|
||||
:resolve-on-load="props.searchOnLoad"
|
||||
v-model="model"
|
||||
:options="search"
|
||||
@@ -53,7 +54,7 @@ const emit = defineEmits(['update:modelValue', 'create'])
|
||||
const props = defineProps({
|
||||
model: {type: String as PropType<EditorSupportedModels>, required: true},
|
||||
|
||||
id: {type: String, required: false, default: Math.floor(Math.random()*10000).toString()},
|
||||
id: {type: String, required: false, default: Math.floor(Math.random() * 10000).toString()},
|
||||
|
||||
limit: {type: Number, default: 25},
|
||||
|
||||
@@ -71,6 +72,7 @@ const props = defineProps({
|
||||
|
||||
label: {type: String, default: ''},
|
||||
hint: {type: String, default: ''},
|
||||
density: {type: String as PropType<''|'compact'|'comfortable'>, default: ''},
|
||||
|
||||
searchOnLoad: {type: Boolean, default: false},
|
||||
})
|
||||
@@ -79,7 +81,7 @@ const props = defineProps({
|
||||
* check if model has a non-standard value attribute defined, if not use "id" as the value attribute
|
||||
*/
|
||||
const itemValue = computed(() => {
|
||||
if(modelClass.value.model.itemValue){
|
||||
if (modelClass.value.model.itemValue) {
|
||||
return modelClass.value.model.itemValue
|
||||
}
|
||||
return 'id'
|
||||
@@ -89,7 +91,7 @@ const itemValue = computed(() => {
|
||||
* check if model has a non-standard label attribute defined, if not use "name" as the value attribute
|
||||
*/
|
||||
const itemLabel = computed(() => {
|
||||
if(modelClass.value.model.itemLabel){
|
||||
if (modelClass.value.model.itemLabel) {
|
||||
return modelClass.value.model.itemLabel
|
||||
}
|
||||
return 'name'
|
||||
@@ -135,7 +137,7 @@ function search(query: string) {
|
||||
* @param select$ reference to multiselect instance
|
||||
*/
|
||||
async function createObject(object: any, select$: Multiselect) {
|
||||
return await modelClass.value.create({name: object[itemLabel.value]}).then((createdObj : any) => {
|
||||
return await modelClass.value.create({name: object[itemLabel.value]}).then((createdObj: any) => {
|
||||
useMessageStore().addMessage(MessageType.SUCCESS, 'Created', 5000, createdObj)
|
||||
emit('create', object)
|
||||
return createdObj
|
||||
@@ -148,17 +150,29 @@ async function createObject(object: any, select$: Multiselect) {
|
||||
</script>
|
||||
|
||||
<style src="@vueform/multiselect/themes/default.css"></style>
|
||||
<style>
|
||||
<style scoped>
|
||||
.material-multiselect {
|
||||
--ms-line-height: 2.3;
|
||||
--ms-bg: rgba(235, 235, 235, 0.3);
|
||||
--ms-bg: rgba(210, 210, 210, 0.1);
|
||||
--ms-border-color: 0;
|
||||
--ms-border-color-active: 0;
|
||||
border-bottom: inset 1px #323232;
|
||||
border-bottom: inset 1px rgba(50, 50, 50, 0.8);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.model-select--density-compact {
|
||||
--ms-line-height: 1.3;
|
||||
}
|
||||
|
||||
.model-select--density-comfortable {
|
||||
--ms-line-height: 1.8;
|
||||
}
|
||||
|
||||
.model-select--density-default {
|
||||
--ms-line-height: 2.3;
|
||||
}
|
||||
|
||||
|
||||
.multiselect-tag {
|
||||
background-color: #b98766 !important;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ import {useDisplay} from "vuetify";
|
||||
import {VueDraggable} from "vue-draggable-plus";
|
||||
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
|
||||
import IngredientString from "@/components/display/IngredientString.vue";
|
||||
import numberInput from "../../../../vue/src/components/Modals/NumberInput.vue";
|
||||
|
||||
const emit = defineEmits(['delete'])
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "",
|
||||
"Ingredient Overview": "",
|
||||
"IngredientInShopping": "",
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"Imported_From": "Внесено от",
|
||||
"Importer_Help": "Повече информация и помощ за този вносител:",
|
||||
"Information": "Информация",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Редактор на съставки",
|
||||
"IngredientInShopping": "Тази съставка е във вашия списък за пазаруване.",
|
||||
"Ingredients": "Съставки",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editor d'ingredients",
|
||||
"Ingredient Overview": "",
|
||||
"IngredientInShopping": "",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"Imported_From": "Importováno z",
|
||||
"Importer_Help": "Nápověda k importu z této aplikace:",
|
||||
"Information": "Informace",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editace ingrediencí",
|
||||
"Ingredient Overview": "Přehled ingrediencí",
|
||||
"IngredientInShopping": "Tato ingredience je na vašem nákupním seznamu.",
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"Imported_From": "Importeret fra",
|
||||
"Importer_Help": "Mere information og hjælp til denne importer:",
|
||||
"Information": "Information",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Ingrediens redigeringsværktøj",
|
||||
"Ingredient Overview": "Ingrediensoversigt",
|
||||
"IngredientInShopping": "Denne ingrediens er i din indkøbsliste.",
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
"Imported_From": "Importiert aus",
|
||||
"Importer_Help": "Zusätzliche Informationen und Hilfe zu diesem Importer:",
|
||||
"Information": "Information",
|
||||
"Ingredient": "Zutat",
|
||||
"Ingredient Editor": "Zutateneditor",
|
||||
"Ingredient Overview": "Zutatenübersicht",
|
||||
"IngredientInShopping": "Diese Zutat befindet sich auf Ihrer Einkaufsliste.",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"Imported_From": "Πηγή",
|
||||
"Importer_Help": "Περισσότερες πληροφορίες και βοήθεια για αυτό το πρόγραμμα εισαγωγής:",
|
||||
"Information": "Πληροφορίες",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Επεξεργαστής συστατικών",
|
||||
"Ingredient Overview": "Σύνοψη υλικών",
|
||||
"IngredientInShopping": "Αυτό το υλικό είναι στη λίστα αγορών.",
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
"Imported_From": "Imported from",
|
||||
"Importer_Help": "More information and help on this importer:",
|
||||
"Information": "Information",
|
||||
"Ingredient": "Ingredient",
|
||||
"Ingredient Editor": "Ingredient Editor",
|
||||
"Ingredient Overview": "Ingredient Overview",
|
||||
"IngredientInShopping": "This ingredient is in your shopping list.",
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
"Imported_From": "Importado de",
|
||||
"Importer_Help": "Más información y ayuda con este importador:",
|
||||
"Information": "Información",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Ingredientes",
|
||||
"Ingredient Overview": "Vistazo de Ingredientes",
|
||||
"IngredientInShopping": "Este ingrediente ya esta en la lista de la compra.",
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"Import": "Tuo",
|
||||
"Import_finished": "Tuonti valmistui",
|
||||
"Information": "Tiedot",
|
||||
"Ingredient": "",
|
||||
"Ingredients": "Ainesosat",
|
||||
"Instructions": "Ohjeet",
|
||||
"InstructionsEditHelp": "",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"Imported_From": "Importé depuis",
|
||||
"Importer_Help": "Plus d'information et d'aide sur cet importateur :",
|
||||
"Information": "Information",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Éditeur d’ingrédients",
|
||||
"Ingredient Overview": "Aperçu des ingrédients",
|
||||
"IngredientInShopping": "Cet ingrédient est dans votre liste de courses.",
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
"Imported_From": "יובא מ",
|
||||
"Importer_Help": "עוד מידע ועזרה על כלי ייבוא זה:",
|
||||
"Information": "מידע",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "עורך המרכיב",
|
||||
"Ingredient Overview": "סקירת רכיב",
|
||||
"IngredientInShopping": "רכיב זה ברשימת הקניות.",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"Imported_From": "Importálva",
|
||||
"Importer_Help": "",
|
||||
"Information": "Információ",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Hozzávalók szerkesztője",
|
||||
"Ingredient Overview": "Hozzávalók áttekintése",
|
||||
"IngredientInShopping": "Ez a hozzávaló szerepel a bevásárlólistán.",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"Import": "Ներմուծել",
|
||||
"Import_finished": "Ներմուծումն ավարտված է",
|
||||
"Information": "Տեղեկություն",
|
||||
"Ingredient": "",
|
||||
"Ingredients": "",
|
||||
"InstructionsEditHelp": "",
|
||||
"Invite_Link": "",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "Informasi",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editor Bahan",
|
||||
"Ingredient Overview": "",
|
||||
"IngredientInShopping": "",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "",
|
||||
"Ingredient Overview": "",
|
||||
"IngredientInShopping": "",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"Imported_From": "Importato da",
|
||||
"Importer_Help": "Per altre informazioni e aiuto su questo importer:",
|
||||
"Information": "Informazioni",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editor Ingredienti",
|
||||
"Ingredient Overview": "Panoramica Ingredienti",
|
||||
"IngredientInShopping": "Questo ingrediente è nella tua lista della spesa.",
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Ingredientų redaktorius",
|
||||
"Ingredient Overview": "",
|
||||
"IngredientInShopping": "",
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "Informasjon",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Ingrediens Behandler",
|
||||
"Ingredient Overview": "",
|
||||
"IngredientInShopping": "Denne ingrediensen er i handlekurven din.",
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
"Imported_From": "Geïmporteerd van",
|
||||
"Importer_Help": "Meer informatie en hulp over de importtool:",
|
||||
"Information": "Informatie",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Ingrediënten editor",
|
||||
"Ingredient Overview": "Ingrediëntenlijst",
|
||||
"IngredientInShopping": "Dit ingrediënt staat op je boodschappenlijst.",
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
"Imported_From": "Zaimportowane z",
|
||||
"Importer_Help": "Więcej informacji i pomoc na temat tego importera:",
|
||||
"Information": "Informacja",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Edytor składników",
|
||||
"Ingredient Overview": "Przegląd składników",
|
||||
"IngredientInShopping": "Ten składnik znajduje się na Twojej liście zakupów.",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"Import": "Importar",
|
||||
"Import_finished": "Importação terminada",
|
||||
"Information": "Informação",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editor de Ingredientes",
|
||||
"IngredientInShopping": "Este ingrediente está na sua lista de compras.",
|
||||
"Ingredients": "Ingredientes",
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"Imported_From": "Importado de",
|
||||
"Importer_Help": "Mais informações neste importador:",
|
||||
"Information": "Informação",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editor de Ingrediente",
|
||||
"Ingredient Overview": "Ingredientes - Visão Geral",
|
||||
"IngredientInShopping": "Este ingrediente está na sua lista de compras.",
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
"Imported_From": "Importat din",
|
||||
"Importer_Help": "Mai multe informații și ajutor cu privire la acest importator:",
|
||||
"Information": "Informație",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Editor de ingrediente",
|
||||
"Ingredient Overview": "Prezentare generală a ingredientelor",
|
||||
"IngredientInShopping": "Acest ingredient se află în lista de cumpărături.",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"Imported": "Импортировано",
|
||||
"Imported_From": "Импортировано из",
|
||||
"Information": "Информация",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Редактор ингредиентов",
|
||||
"IngredientInShopping": "Этот ингредиент в вашем списке покупок.",
|
||||
"Ingredients": "Ингредиенты",
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"Import": "Uvozi",
|
||||
"Import_finished": "Uvoz je končan",
|
||||
"Information": "Informacija",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Urejevalnik Sestavin",
|
||||
"IngredientInShopping": "Ta sestavina je v tvojem nakupovalnem listku.",
|
||||
"Ingredients": "Sestavine",
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
"Imported_From": "Importerad från",
|
||||
"Importer_Help": "Mer information och hjälp om denna import:",
|
||||
"Information": "Information",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Ingrediensredigerare",
|
||||
"Ingredient Overview": "Ingrediensöversikt",
|
||||
"IngredientInShopping": "Denna ingrediens finns i din inköpslista.",
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
"Imported_From": "İçe Aktarıldığı Yer",
|
||||
"Importer_Help": "Bu içe aktarıcı hakkında daha fazla bilgi ve yardım:",
|
||||
"Information": "Bilgi",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Malzeme Düzenleyici",
|
||||
"Ingredient Overview": "Malzeme Genel Bakış",
|
||||
"IngredientInShopping": "Bu malzeme alışveriş listenizde.",
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"Imported_From": "",
|
||||
"Importer_Help": "",
|
||||
"Information": "Інформація",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "Редактор Інгредієнтів",
|
||||
"IngredientInShopping": "Цей інгредієнт є в вашому списку покупок.",
|
||||
"Ingredients": "Інгредієнти",
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
"Imported_From": "导入",
|
||||
"Importer_Help": "有关此进口商的更多信息和帮助:",
|
||||
"Information": "更多信息",
|
||||
"Ingredient": "",
|
||||
"Ingredient Editor": "食材编辑器",
|
||||
"Ingredient Overview": "食材概述",
|
||||
"IngredientInShopping": "此食材已在购物清单中。",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Import": "",
|
||||
"Import_finished": "匯入完成",
|
||||
"Information": "",
|
||||
"Ingredient": "",
|
||||
"Ingredients": "",
|
||||
"InstructionsEditHelp": "",
|
||||
"Invite_Link": "",
|
||||
|
||||
246
vue3/src/pages/IngredientEditorPage.vue
Normal file
246
vue3/src/pages/IngredientEditorPage.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card :loading="filtersLoading">
|
||||
<v-card-title>{{ $t('Ingredient Editor') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<closable-help-alert
|
||||
class="mb-2"
|
||||
text="With the ingredient editor you can edit all Ingredients that use a certain Food and/or Unit at once. This can be used to easily correct errors or change multiple recipes at once."></closable-help-alert>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<model-select model="Food" v-model="selectedFood" @update:modelValue="refreshPage()" append-to-body></model-select>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<model-select model="Unit" v-model="selectedUnit" @update:modelValue="refreshPage()" append-to-body></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mt-2">
|
||||
<v-data-table-server
|
||||
@update:options="loadItems"
|
||||
:items="items"
|
||||
:items-length="tableItemCount"
|
||||
:items-per-page="tablePageSize"
|
||||
:headers="tableHeaders"
|
||||
:expanded="items.flatMap((i:Ingredient) => i.id)"
|
||||
:page="tablePage"
|
||||
:loading="ingredientsLoading"
|
||||
>
|
||||
<template v-slot:header.action="{ column }">
|
||||
<v-btn size="small" color="save" @click="updateAllIngredients()">
|
||||
<v-icon icon="$save"></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:expanded-row="{ columns, item }">
|
||||
<tr>
|
||||
<td :colspan="columns.length">
|
||||
<v-btn variant="outlined" color="secondary" target="_blank" :to="{name: 'view_recipe', params: {id: r.id}}" v-for="r in item.usedInRecipes">
|
||||
{{ r.name }}
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.amount="{ item }">
|
||||
<v-number-input :label="$t('Amount')" v-model="item.amount" inset control-variant="stacked" hide-details :min="0" density="compact"
|
||||
@update:modelValue="item.changed = true"></v-number-input>
|
||||
</template>
|
||||
<template v-slot:item.unit="{ item }">
|
||||
<model-select model="Unit" v-model="item.unit" :label="$t('Unit')" density="compact" hide-details allow-create append-to-body
|
||||
@update:modelValue="item.changed = true"></model-select>
|
||||
</template>
|
||||
<template v-slot:item.food="{ item }">
|
||||
<model-select model="Food" v-model="item.food" :label="$t('Food')" density="compact" hide-details allow-create append-to-body
|
||||
@update:modelValue="item.changed = true"></model-select>
|
||||
</template>
|
||||
<template v-slot:item.note="{ item }">
|
||||
<v-text-field v-model="item.note" :label="$t('Note')" density="compact" hide-details @update:modelValue="item.changed = true"></v-text-field>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.action="{ item }">
|
||||
<v-btn-group density="comfortable">
|
||||
<v-btn size="small" color="save" :loading="item.loading" @click="updateIngredient(item)" :disabled="!item.changed">
|
||||
<v-icon icon="$save" ></v-icon>
|
||||
</v-btn>
|
||||
<v-btn size="small" color="delete" :loading="item.loading">
|
||||
<v-icon icon="$delete"></v-icon>
|
||||
<delete-confirm-dialog :model-name="$t('Ingredient')" @delete="deleteIngredient(item)"></delete-confirm-dialog>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</template>
|
||||
|
||||
</v-data-table-server>
|
||||
|
||||
</v-card>
|
||||
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
import ClosableHelpAlert from "@/components/display/ClosableHelpAlert.vue";
|
||||
import {ApiApi, ApiIngredientListRequest, Food, Ingredient, Unit} from "@/openapi";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {useUrlSearchParams} from "@vueuse/core";
|
||||
import {VNumberInput} from 'vuetify/labs/VNumberInput'
|
||||
import DeleteConfirmDialog from "@/components/dialogs/DeleteConfirmDialog.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
const params = useUrlSearchParams('history', {})
|
||||
|
||||
type EditorIngredient = Ingredient & { changed: boolean, loading: boolean }
|
||||
|
||||
const items = ref([] as EditorIngredient[])
|
||||
|
||||
const tableHeaders = [
|
||||
{title: t('Amount'), key: 'amount', minWidth: '120px', cellProps: {class: 'pr-0'}},
|
||||
{title: t('Unit'), key: 'unit', minWidth: '120px', cellProps: {class: 'pr-0'}},
|
||||
{title: t('Food'), key: 'food', minWidth: '120px', cellProps: {class: 'pr-0'}},
|
||||
{title: t('Note'), key: 'note', minWidth: '120px', cellProps: {class: 'pr-0'}},
|
||||
{key: 'action', width: '1%', noBreak: true, align: 'end'},
|
||||
]
|
||||
|
||||
const tablePage = ref(1)
|
||||
const tablePageSize = ref(25)
|
||||
const tableItemCount = ref(0)
|
||||
|
||||
const ingredientsLoading = ref(false)
|
||||
const filtersLoading = ref(true)
|
||||
|
||||
const selectedFood = ref<null | Food>(null)
|
||||
const selectedUnit = ref<null | Unit>(null)
|
||||
|
||||
const deleteConfirmDialog = ref(false)
|
||||
const deleteConfirmIngredient = ref({} as EditorIngredient)
|
||||
|
||||
onMounted(() => {
|
||||
getAndLoadParameters()
|
||||
})
|
||||
|
||||
/**
|
||||
* update all changed ingredients
|
||||
*/
|
||||
function updateAllIngredients() {
|
||||
items.value.forEach((item) => {
|
||||
if (item.changed) {
|
||||
updateIngredient(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* update a single ingredient in the database
|
||||
* @param ingredient
|
||||
*/
|
||||
function updateIngredient(ingredient: EditorIngredient) {
|
||||
let api = new ApiApi()
|
||||
ingredient.loading = true
|
||||
api.apiIngredientUpdate({id: ingredient.id!, ingredient: ingredient}).then(r => {
|
||||
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||
}).finally(() => {
|
||||
ingredient.loading = false
|
||||
ingredient.changed = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* delete the given ingredient form the database and local client data
|
||||
* @param ingredient
|
||||
*/
|
||||
function deleteIngredient(ingredient: EditorIngredient) {
|
||||
let api = new ApiApi()
|
||||
ingredient.loading = true
|
||||
api.apiIngredientDestroy({id: ingredient.id!}).then(r => {
|
||||
items.value = items.value.filter(i => i.id != ingredient.id)
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
|
||||
}).finally(() => {
|
||||
ingredient.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* use URL parameters to retrieve associated food or unit, after that load table data
|
||||
*/
|
||||
function getAndLoadParameters() {
|
||||
let api = new ApiApi()
|
||||
let promises: Promise<any>[] = []
|
||||
filtersLoading.value = true
|
||||
|
||||
if (params.food_id && !Number.isNaN(params.food_id)) {
|
||||
promises.push(api.apiFoodRetrieve({id: Number(params.food_id)}).then(r => {
|
||||
selectedFood.value = r
|
||||
}))
|
||||
}
|
||||
|
||||
if (params.unit_id && !Number.isNaN(params.unit_id)) {
|
||||
promises.push(api.apiUnitRetrieve({id: Number(params.unit_id)}).then(r => {
|
||||
selectedUnit.value = r
|
||||
}))
|
||||
}
|
||||
|
||||
Promise.allSettled(promises).then(() => {
|
||||
filtersLoading.value = false
|
||||
if (params.food_id || params.unit_id) {
|
||||
refreshPage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* manually trigger item load
|
||||
*/
|
||||
function refreshPage() {
|
||||
loadItems({page: tablePage.value, itemsPerPage: tablePageSize.value})
|
||||
}
|
||||
|
||||
/**
|
||||
* load items from server
|
||||
* @param page
|
||||
* @param itemsPerPage
|
||||
* @param search
|
||||
* @param sortBy
|
||||
* @param groupBy
|
||||
*/
|
||||
function loadItems({page, itemsPerPage, search, sortBy, groupBy}) {
|
||||
// never load unfiltered, only load if at least one filter is set
|
||||
if (!selectedFood.value && !selectedUnit.value) {
|
||||
return
|
||||
}
|
||||
|
||||
let api = new ApiApi()
|
||||
ingredientsLoading.value = true
|
||||
|
||||
let requestParameters: ApiIngredientListRequest = {page: page, pageSize: itemsPerPage}
|
||||
if (selectedFood.value) {
|
||||
requestParameters.food = selectedFood.value.id!
|
||||
}
|
||||
|
||||
if (selectedUnit.value) {
|
||||
requestParameters.unit = selectedUnit.value.id!
|
||||
}
|
||||
|
||||
api.apiIngredientList(requestParameters).then(r => {
|
||||
items.value = r.results
|
||||
tableItemCount.value = r.count
|
||||
}).catch(err => {
|
||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||
}).finally(() => {
|
||||
ingredientsLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -51,10 +51,16 @@
|
||||
<v-list-item prepend-icon="$edit" :to="{name: 'ModelEditPage', params: {model: model, id: item.id}}">
|
||||
{{ $t('Edit') }}
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" link>
|
||||
<v-list-item prepend-icon="fa-solid fa-arrows-to-dot" v-if="genericModel.model.isMerge" link>
|
||||
{{ $t('Merge') }}
|
||||
<model-merge-dialog :model="model" :source="item" @change="loadItems({page: tablePage, itemsPerPage: useUserPreferenceStore().deviceSettings.general_tableItemsPerPage})"></model-merge-dialog>
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-table-list" :to="{name: 'IngredientEditorPage', query: {food_id: item.id}}" v-if="genericModel.model.name == 'Food'">
|
||||
{{ $t('Ingredient Editor') }}
|
||||
</v-list-item>
|
||||
<v-list-item prepend-icon="fa-solid fa-table-list" :to="{name: 'IngredientEditorPage', query: {unit_id: item.id}}" v-if="genericModel.model.name == 'Unit'">
|
||||
{{ $t('Ingredient Editor') }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
@@ -105,12 +111,6 @@ const itemsPerPageOptions = [
|
||||
{value: 50, title: '50'},
|
||||
]
|
||||
|
||||
const tableHeaders: VDataTableProps['headers'] = [
|
||||
{title: t('Name'), key: 'name'},
|
||||
{title: t('Category'), key: 'supermarketCategory.name'},
|
||||
{title: t('Actions'), key: 'action', align: 'end'},
|
||||
]
|
||||
|
||||
const tablePage = ref(1)
|
||||
const tablePageInitialized = ref(false) // TODO workaround until vuetify bug is fixed
|
||||
|
||||
@@ -175,6 +175,10 @@ function loadItems({page, itemsPerPage, search, sortBy, groupBy}) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* change models and reset page/scroll
|
||||
* @param m
|
||||
*/
|
||||
function changeModel(m: Model) {
|
||||
tablePage.value = 1
|
||||
router.push({name: 'ModelListPage', params: {model: m.name.toLowerCase()}})
|
||||
|
||||
@@ -12,6 +12,40 @@
|
||||
<model-edit-dialog model="MealPlan" v-model="dialog" :item="defaultItem" :activator="activator"></model-edit-dialog>
|
||||
</v-btn>
|
||||
|
||||
<v-row class="mt-5">
|
||||
<v-col>
|
||||
<v-text-field density="compact"></v-text-field>
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
<model-select model="Food" density="compact"></model-select>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-5">
|
||||
<v-col>
|
||||
|
||||
<v-text-field density="comfortable"></v-text-field>
|
||||
|
||||
</v-col>
|
||||
<v-col>
|
||||
|
||||
<model-select model="Food" density="comfortable"></model-select>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-5">
|
||||
<v-col>
|
||||
|
||||
<v-text-field></v-text-field>
|
||||
</v-col>
|
||||
<v-col>
|
||||
|
||||
<model-select model="Food"></model-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -21,6 +55,7 @@ import {ApiApi, MealPlan} from "@/openapi";
|
||||
import {ref, useTemplateRef} from "vue";
|
||||
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
|
||||
import {DateTime} from "luxon";
|
||||
import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
||||
|
||||
const image = ref(File)
|
||||
const response = ref('')
|
||||
|
||||
Reference in New Issue
Block a user