1
0
mirror of https://github.com/TandoorRecipes/recipes.git synced 2026-01-11 17:16:59 -05:00
Files
recipes/vue3/src/pages/IngredientEditorPage.vue
2025-01-03 23:14:16 +01:00

343 lines
14 KiB
Vue

<template>
<v-container>
<v-card :loading="filtersLoading">
<v-card-title>{{ $t('Ingredient Editor') }}</v-card-title>
<v-card-text>
<v-row>
<v-col>
<closable-help-alert
class="mb-2"
:text="$t('IngredientEditorHelp')"></closable-help-alert>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<model-select model="Food" v-model="selectedFood" @update:modelValue="refreshPage()" append-to-body>
<template #append>
<v-btn icon variant="plain">
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent">
<v-list density="compact">
<v-list-item link prepend-icon="$edit" :disabled="!selectedFood">
{{ $t('Edit') }}
<model-edit-dialog model="Food" :item="selectedFood" activator="parent" @save="(obj: Food) => {selectedFood = obj}"
@delete="selectedFood = null; refreshPage()"></model-edit-dialog>
</v-list-item>
<v-list-item link prepend-icon="fa-solid fa-arrows-to-dot" :disabled="!selectedFood">
{{ $t('Merge') }}
<model-merge-dialog :source="selectedFood" model="Food"
@change="(obj: Food) => {selectedFood = obj;refreshPage()} "></model-merge-dialog>
</v-list-item>
<v-list-item link prepend-icon="$delete" :disabled="!selectedFood">
{{ $t('Delete') }}
<delete-confirm-dialog :model-name="$t('Food')" :object-name="selectedFood.name" v-if="selectedFood"
@delete="deleteFood()"></delete-confirm-dialog>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn icon="fa-solid fa-carrot" :to="{name: 'ModelListPage', params: {model: 'food'}}" variant="plain"></v-btn>
</template>
</model-select>
</v-col>
<v-col cols="12" md="6">
<model-select model="Unit" v-model="selectedUnit" @update:modelValue="refreshPage()" append-to-body>
<template #append>
<v-btn icon variant="plain">
<v-icon icon="$menu"></v-icon>
<v-menu activator="parent">
<v-list density="compact">
<v-list-item link prepend-icon="$edit" :disabled="!selectedUnit">
{{ $t('Edit') }}
<model-edit-dialog model="Unit" :item="selectedUnit" activator="parent" @save="(obj: Food) => {selectedUnit = obj}"
@delete="selectedUnit = null; refreshPage()"></model-edit-dialog>
</v-list-item>
<v-list-item link prepend-icon="fa-solid fa-arrows-to-dot" :disabled="!selectedUnit">
{{ $t('Merge') }}
<model-merge-dialog :source="selectedUnit" model="Unit"
@change="(obj: Food) => {selectedUnit = obj;refreshPage()} "></model-merge-dialog>
</v-list-item>
<v-list-item link prepend-icon="$delete" :disabled="!selectedUnit">
{{ $t('Delete') }}
<delete-confirm-dialog :model-name="$t('Unit')" :object-name="selectedUnit.name" v-if="selectedUnit"
@delete="deleteUnit()"></delete-confirm-dialog>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn icon="fa-solid fa-scale-balanced" :to="{name: 'ModelListPage', params: {model: 'unit'}}" variant="plain"></v-btn>
</template>
</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"
disable-sort
>
<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 }} (#{{ r.id }})
</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";
import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import ModelMergeDialog from "@/components/dialogs/ModelMergeDialog.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)
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) {
items.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
})
}
/**
* delete the selected food
*/
function deleteFood() {
let api = new ApiApi()
if (selectedFood.value) {
filtersLoading.value = true
api.apiFoodDestroy({id: selectedFood.value.id!}).then(r => {
selectedFood.value = null
refreshPage()
}).catch(err => {
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
}).finally(() => {
filtersLoading.value = false
})
}
}
/**
* delete the selected unit
*/
function deleteUnit() {
let api = new ApiApi()
if (selectedUnit.value) {
filtersLoading.value = true
api.apiUnitDestroy({id: selectedUnit.value.id!}).then(r => {
selectedUnit.value = null
refreshPage()
}).catch(err => {
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
}).finally(() => {
filtersLoading.value = false
})
}
}
</script>
<style scoped>
</style>