<template> <q-table v-model:pagination="pagination" title="Kalkulationstabelle" :columns="columns" :rows="drinks" :visible-columns="visibleColumn" :dense="$q.screen.lt.md" row-key="id" virtual-scroll :rows-per-page-options="[0]" :filter="search" :filter-method="filter" > <template #header="props"> <q-tr :props="props"> <q-th auto-width /> <q-th v-for="col in props.cols" :key="col.name" :props="props"> {{ col.label }} </q-th> </q-tr> </template> <template #top-right> <div class="row justify-end q-gutter-sm"> <search-input v-model="search" :keys="[...columns, ...column_calc, ...column_prices]" /> <q-btn label="Aufpreise"> <q-menu anchor="center middle" self="center middle"> <min-price-setting /> </q-menu> </q-btn> <q-btn label="neues Getränk" color="positive" icon-right="add"> <q-menu v-model="showNewDrink" anchor="center middle" self="center middle" persistent> <new-drink @close="showNewDrink = false" /> </q-menu> </q-btn> <q-select v-model="visibleColumn" multiple filled dense options-dense display-value="Sichtbarkeit" emit-value map-options :options="[...columns, ...column_calc, ...column_prices]" option-value="name" options-cover /> </div> </template> <template #body="drinks_props"> <q-tr :props="drinks_props"> <q-td auto-width> <q-btn v-if="drinks_props.row.volumes.length === 0" size="xs" color="negative" round dense icon="mdi-delete" class="q-mx-sm" @click="deleteDrink(drinks_props.row)" /> </q-td> <q-td key="picture" :props="drinks_props" style="min-width: 256px"> <q-img loading="lazy" :src=" drinks_props.row.uuid ? `/api/pricelist/picture/${drinks_props.row.uuid}?size=256` : 'no-image.svg' " placeholder-src="no-image.svg" > <template #error> <div class="absolute-full flex flex-center bg-negative text-white"> Cannot load image </div> </template> </q-img> <q-popup-edit v-model="drinkPic" @update:modelValue="savePicture(drinks_props.row)"> <template #default="scope"> <div class="full-width row"> <q-file v-model="scope.value" filled> <template #prepend> <q-icon name="attach_file" /> </template> </q-file> <div class="full-width row justify-between"> <q-btn label="Abbrechen" flat color="primary" @click="scope.cancel" /> <q-btn label="Löschen" flat color="primary" @click=" scope.cancel(); deletePicture(drinks_props.row); " /> <q-btn label="Speichern" flat color="primary" @click="scope.set" /> </div> </div> </template> </q-popup-edit> </q-td> <q-td key="name" :props="drinks_props"> {{ drinks_props.row.name }} <q-popup-edit v-slot="scope" v-model="drinks_props.row.name" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model="scope.value" filled dense autofocus clearable @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="drink_type" :props="drinks_props"> {{ drinks_props.row.type.name }} <q-popup-edit v-slot="scope" v-model="drinks_props.row.type" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-select v-model="scope.value" :options="drinkTypes" option-label="name" option-value="id" filled dense autofocus @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="tags" :props="drinks_props"> <q-badge v-for="tag in drinks_props.row.tags" :key="`${drinks_props.row.id}-${tag.id}`" class="q-ma-xs" rounded :style="`background-color: ${tag.color}`" > {{ tag.name }} </q-badge> <q-popup-edit v-model="drinks_props.row.tags" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <template #default="scope"> <q-select v-model="scope.value" multiple :options="tags" label="Tags" option-label="name" filled > <template #selected-item="item"> <q-chip removable :tabindex="item.tabindex" :style="`background-color: ${item.opt.color}`" @remove="item.removeAtIndex(item.index)" > {{ item.opt.name }} </q-chip> </template> <template #option="item"> <q-item v-bind="item.itemProps" v-on="item.itemEvents"> <q-chip :style="`background-color: ${item.opt.color}`"> <q-avatar v-if="item.selected" icon="mdi-check" color="positive" text-color="white" /> {{ item.opt.name }} </q-chip> </q-item> </template> </q-select> </template> </q-popup-edit> </q-td> <q-td key="article_id" :props="drinks_props"> {{ drinks_props.row.article_id || 'o.A.' }} <q-popup-edit v-slot="scope" v-model="drinks_props.row.article_id" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model="scope.value" filled dense autofocus clearable @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="volume_package" :props="drinks_props"> {{ drinks_props.row.volume ? `${drinks_props.row.volume} L` : 'o.A.' }} <q-popup-edit v-if=" !drinks_props.row.volumes.some((volume) => volume.ingredients.some((ingredient) => ingredient.drink_ingredient) ) " v-slot="scope" v-model.number="drinks_props.row.volume" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model.number="scope.value" filled dense autofocus type="number" clearable step="0.01" min="0" suffix="L" @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="package_size" :props="drinks_props"> {{ drinks_props.row.package_size || 'o.A.' }} <q-popup-edit v-if=" !drinks_props.row.volumes.some((volume) => volume.ingredients.some((ingredient) => ingredient.drink_ingredient) ) " v-slot="scope" v-model="drinks_props.row.package_size" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model.number="scope.value" filled dense autofocus type="number" min="0" @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="cost_per_package" :props="drinks_props"> {{ drinks_props.row.cost_per_package ? `${drinks_props.row.cost_per_package.toFixed(2)}€` : 'o.A.' }} <q-popup-edit v-if=" !drinks_props.row.volumes.some((volume) => volume.ingredients.some((ingredient) => ingredient.drink_ingredient) ) " v-slot="scope" v-model="drinks_props.row.cost_per_package" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model.number="scope.value" filled dense autofocus type="number" step="0.01" min="0" suffix="€" @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="cost_per_volume" :props="drinks_props"> {{ drinks_props.row.cost_per_volume ? `${drinks_props.row.cost_per_volume.toFixed(3)}€` : 'o.A.' }} <q-popup-edit v-if=" !( !!drinks_props.row.cost_per_package && !!drinks_props.row.volume && !!drinks_props.row.package_size ) && !drinks_props.row.volumes.some((volume) => volume.ingredients.some((ingredient) => ingredient.drink_ingredient) ) " v-slot="scope" v-model="drinks_props.row.cost_per_volume" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model.number="scope.value" filled dense autofocus type="number" min="0" step="0.1" suffix="€" @keyup.enter="scope.set" /> </q-popup-edit> </q-td> <q-td key="volumes" :props="drinks_props"> <drink-price-volumes-table :rows="drinks_props.row.volumes" :visible-columns="visibleColumn" :columns="column_calc" :drink="drinks_props.row" @updateDrink="updateDrink(drinks_props.row)" /> </q-td> <q-td key="receipt" :props="drinks_props"> <build-manual :steps="drinks_props.row.receipt" @deleteStep="deleteStep($event, drinks_props.row)" @addStep="addStep($event, drinks_props.row)" /> <!--<q-popup-edit v-slot="scope" v-model="drinks_props.row.receipt" buttons label-cancel="Abbrechen" label-set="Speichern" @update:modelValue="updateDrink(drinks_props.row)" > <q-input v-model="scope.value" type="textarea" autofocus counter @keyup.enter.stop /> </q-popup-edit>--> </q-td> </q-tr> </template> </q-table> </template> <script lang="ts"> import { defineComponent, onBeforeMount, ComputedRef, computed, ref } from 'vue'; import DrinkPriceVolumesTable from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumesTable.vue'; import { useMainStore } from 'src/stores'; import { Drink, usePricelistStore } from 'src/plugins/pricelist/store'; import MinPriceSetting from 'src/plugins/pricelist/components/MinPriceSetting.vue'; import NewDrink from 'src/plugins/pricelist/components/CalculationTable/NewDrink.vue'; import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue'; import SearchInput from './SearchInput.vue'; import { filter, Search } from '../utils/filter'; import { Notify } from 'quasar'; import { sort } from '../utils/sort'; export default defineComponent({ name: 'CalculationTable', components: { SearchInput, BuildManual, MinPriceSetting, DrinkPriceVolumesTable, NewDrink }, setup() { const mainStore = useMainStore(); const store = usePricelistStore(); onBeforeMount(() => { void store.getDrinkTypes(true); void store.getTags(); void store.getExtraIngredients(); void store.get_min_prices(); store.getPriceCalcColumn(user); }); const user = mainStore.currentUser.userid; const columns = [ { name: 'picture', label: 'Bild', }, { name: 'name', label: 'Getränkename', field: 'name', sortable: true, sort, }, { name: 'drink_type', label: 'Kategorie', field: 'type', format: (val: FG.DrinkType) => `${val.name}`, sortable: true, sort: (a: FG.DrinkType, b: FG.DrinkType) => sort(a.name, b.name), }, { name: 'tags', label: 'Tags', field: 'tags', format: (val: Array<FG.Tag>) => { let retVal = ''; val.forEach((tag, index) => { if (index > 0) { retVal += ', '; } retVal += tag.name; }); return retVal; }, }, { name: 'article_id', label: 'Artikelnummer', field: 'article_id', sortable: true, sort, }, { name: 'volume_package', label: 'Inhalt in l des Gebinde', field: 'volume', sortable: true, sort, }, { name: 'package_size', label: 'Gebindegröße', field: 'package_size', sortable: true, sort, }, { name: 'cost_per_package', label: 'Preis Netto/Gebinde', field: 'cost_per_package', format: (val: number | null) => (val ? `${val.toFixed(3)}€` : ''), sortable: true, sort, }, { name: 'cost_per_volume', label: 'Preis mit 19%/Liter', field: 'cost_per_volume', format: (val: number | null) => (val ? `${val.toFixed(3)}€` : ''), sortable: true, sort: (a: ComputedRef, b: ComputedRef) => sort(a.value, b.value), }, { name: 'volumes', label: 'Preiskalkulation', field: 'volumes', }, { name: 'receipt', label: 'Bauanleitung', field: 'receipt', }, ]; const column_calc = [ { name: 'volume', label: 'Abgabe in l', field: 'volume', }, { name: 'min_prices', label: 'Minimal Preise', field: 'min_prices', }, { name: 'prices', label: 'Preise', field: 'prices', }, ]; const column_prices = [ { name: 'price', label: 'Preis', field: 'price', format: (val: number) => `${val.toFixed(2)}€`, }, { name: 'description', label: 'Beschreibung', field: 'description', }, { name: 'public', label: 'Öffentlich', field: 'public', }, ]; const visibleColumn = computed({ get: () => store.pricecalc_columns, set: (val) => { store.updatePriceCalcColumn(user, val); }, }); // eslint-disable-next-line vue/return-in-computed-property const pagination = computed(() => { rowsPerPage: store.drinks.length; }); const drinkTypes = computed(() => store.drinkTypes); function updateDrink(drink: Drink) { void store.updateDrink(drink); } function deleteDrink(drink: Drink) { store.deleteDrink(drink); } const showNewDrink = ref(false); const drinkPic = ref<File>(); function onPictureRejected() { Notify.create({ group: false, type: 'negative', message: 'Datei zu groß oder keine gültige Bilddatei.', timeout: 10000, progress: true, actions: [{ icon: 'mdi-close', color: 'white' }], }); drinkPic.value = undefined; } function savePicture(drink: Drink) { console.log('hier bin ich!!!', drinkPic.value); if (drinkPic.value && drinkPic.value instanceof File) { store.upload_drink_picture(drink, drinkPic.value).catch((response: Response) => { if (response && response.status == 400) { onPictureRejected(); } }); } } function deletePicture(drink: Drink) { void store.delete_drink_picture(drink); } function addStep(event: string, drink: Drink) { console.log(event, drink.receipt); drink.receipt?.push(event); updateDrink(drink); } function deleteStep(event: number, drink: Drink) { console.log(event, drink.receipt); drink.receipt?.splice(event, 1); updateDrink(drink); } const search = ref<Search>({ value: '', key: '', label: '', }); return { drinks: computed(() => store.drinks), pagination, columns, column_calc, column_prices, visibleColumn, drinkTypes, updateDrink, deleteDrink, showNewDrink, drinkPic, savePicture, deletePicture, addStep, deleteStep, console, search, filter, tags: computed(() => store.tags), }; }, }); </script> <style scoped></style>