<template> <q-table v-model:pagination="pagination" title="Preistabelle" :columns="columns" :rows="drinks" dense :filter="search" :filter-method="filter" grid :rows-per-page-options="[0]" > <template #top-right> <div class="row justify-end q-gutter-sm"> <search-input v-model="search" :keys="search_keys" /> <slot></slot> <q-btn v-if="!public && !nodetails" label="Aufpreise"> <q-menu anchor="center middle" self="center middle"> <min-price-setting /> </q-menu> </q-btn> <q-btn v-if="!public && !nodetails && editable && hasPermission(PERMISSIONS.CREATE)" color="primary" round icon="mdi-plus" @click="newDrink" > <q-tooltip> Neues Getränk </q-tooltip> </q-btn> </div> </template> <template #item="props"> <div class="q-pa-xs col-xs-12 col-sm-6 col-md-4"> <q-card> <q-img style="max-height: 256px" :src=" props.row.uuid ? `/api/pricelist/picture/${props.row.uuid}?size=400` : 'no-image.svg' " > <div v-if="!public && !nodetails && editable" class="absolute-top-right justify-end" style="background-color: transparent" > <q-btn round icon="mdi-pencil" style="background-color: rgba(0, 0, 0, 0.5)" @click="editDrink = props.row" /> </div> <div class="absolute-bottom-right justify-end"> <div class="text-subtitle1 text-right"> {{ props.row.name }} </div> <div class="text-caption text-right"> {{ props.row.type.name }} </div> </div> </q-img> <q-card-section> <q-badge v-for="tag in props.row.tags" :key="`${props.row.id}-${tag.id}`" class="text-caption" rounded :style="`background-color: ${tag.color}`" > {{ tag.name }} </q-badge> </q-card-section> <q-card-section v-if="!public && !nodetails"> <div class="fit row"> <q-input v-if="props.row.article_id" class="col-xs-12 col-sm-6 q-pa-sm" :model-value="props.row.article_id" outlined readonly label="Artikelnummer" dense /> <q-input v-if="props.row.volume" class="col-xs-12 col-sm-6 q-pa-sm" :model-value="props.row.volume" outlined readonly label="Inhalt" dense suffix="L" /> <q-input v-if="props.row.package_size" class="col-xs-12 col-sm-6 q-pa-sm" :model-value="props.row.package_size" outlined readonly label="Gebindegröße" dense /> <q-input v-if="props.row.cost_per_package" class="col-xs-12 col-sm-6 q-pa-sm" :model-value="props.row.cost_per_package" outlined readonly label="Preis Gebinde" suffix="€" dense /> <q-input v-if="props.row.cost_per_volume" class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg" :model-value="props.row.cost_per_volume" outlined readonly label="Preis pro L" hint="Inkl. 19% Mehrwertsteuer" suffix="€" dense /> </div> </q-card-section> <q-card-section v-if="props.row.volumes.length > 0 && notLoading"> <drink-price-volumes :model-value="props.row.volumes" :public="public" :nodetails="nodetails" /> </q-card-section> </q-card> </div> </template> </q-table> <q-dialog :model-value="editDrink !== undefined" persistent> <drink-modify :drink="editDrink" @save="editing_drink" @cancel="editDrink = undefined" @delete="deleteDrink" /> </q-dialog> </template> <script lang="ts"> import { defineComponent, onBeforeMount, ComputedRef, computed, ref } from 'vue'; import { Drink, usePricelistStore, DrinkPriceVolume } from 'src/plugins/pricelist/store'; import MinPriceSetting from 'src/plugins/pricelist/components/MinPriceSetting.vue'; import SearchInput from './SearchInput.vue'; import DrinkPriceVolumes from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumes.vue'; import DrinkModify from './DrinkModify.vue'; import { filter, Search } from '../utils/filter'; import { Notify } from 'quasar'; import { sort } from '../utils/sort'; import { DeleteObjects } from 'src/plugins/pricelist/utils/utils'; import { hasPermission } from 'src/utils/permission'; import { PERMISSIONS } from 'src/plugins/pricelist/permissions'; export default defineComponent({ name: 'CalculationTable', components: { SearchInput, MinPriceSetting, DrinkPriceVolumes, DrinkModify, }, props: { public: { type: Boolean, default: false, }, editable: { type: Boolean, default: false, }, nodetails: { type: Boolean, default: false, }, }, setup(props) { const store = usePricelistStore(); onBeforeMount(() => { void store.getDrinks(); }); const columns = [ { name: 'picture', label: 'Bild', }, { name: 'name', label: 'Name', field: 'name', sortable: true, sort, filterable: true, public: true, }, { 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), filterable: true, public: true, }, { 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; }, filterable: true, public: true, }, { name: 'article_id', label: 'Artikelnummer', field: 'article_id', sortable: true, sort, filterable: true, public: false, }, { name: 'volume_package', label: 'Inhalt in l des Gebinde', field: 'volume', sortable: true, sort, public: false, }, { name: 'package_size', label: 'Gebindegröße', field: 'package_size', sortable: true, sort, public: false, }, { name: 'cost_per_package', label: 'Preis Netto/Gebinde', field: 'cost_per_package', format: (val: number | null) => (val ? `${val.toFixed(3)}€` : ''), sortable: true, sort, public: false, }, { 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', format: (val: Array<DrinkPriceVolume>) => { let retVal = ''; val.forEach((val, index) => { if (index > 0) { retVal += ', '; } retVal += val.id; }); return retVal; }, sortable: false, }, { name: 'receipt', label: 'Bauanleitung', field: 'receipt', format: (val: Array<string>) => { let retVal = ''; val.forEach((value, index) => { if (index > 0) { retVal += ', '; } retVal += value; }); return retVal; }, filterable: true, sortable: false, public: false, }, ]; 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 search_keys = computed(() => columns.filter( (column) => column.filterable && (props.public || props.nodetails ? column.public : true) ) ); const pagination = ref({ sortBy: 'name', descending: false, rowsPerPage: store.drinks.length, }); const drinkTypes = computed(() => store.drinkTypes); function updateDrink(drink: Drink) { void store.updateDrink(drink); } function deleteDrink() { if (editDrink.value) { store.deleteDrink(editDrink.value); } editDrink.value = undefined; } 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; } async function savePicture(drinkPic: File) { if (editDrink.value) { await store.upload_drink_picture(editDrink.value, drinkPic).catch((response: Response) => { if (response && response.status == 400) { onPictureRejected(); } }); } } async function deletePicture() { if (editDrink.value) { await store.delete_drink_picture(editDrink.value); } } const search = ref<Search>({ value: '', key: '', label: '', }); const emptyDrink: Drink = { id: -1, article_id: undefined, package_size: undefined, name: '', volume: undefined, cost_per_volume: undefined, cost_per_package: undefined, tags: [], type: undefined, volumes: [], uuid: '', }; function newDrink() { editDrink.value = Object.assign({}, emptyDrink); } const editDrink = ref<Drink>(); async function editing_drink( drink: Drink, toDeleteObjects: DeleteObjects, drinkPic: File | undefined, deletePic: boolean ) { notLoading.value = false; for (const ingredient of toDeleteObjects.ingredients) { await store.deleteIngredient(ingredient); } for (const price of toDeleteObjects.prices) { await store.deletePrice(price); } for (const volume of toDeleteObjects.volumes) { await store.deleteVolume(volume, drink); } console.log(drink); if (drink.id > 0) { await store.updateDrink(drink); } else { const _drink = await store.setDrink(drink); if (editDrink.value) { editDrink.value.id = _drink.id; } } if (deletePic) { await deletePicture(); } if (drinkPic instanceof File) { await savePicture(drinkPic); } editDrink.value = undefined; notLoading.value = true; } function get_volumes(drink_id: number) { return store.drinks.find((a) => a.id === drink_id)?.volumes; } const notLoading = ref(true); const imageloading = ref<Array<{ id: number; loading: boolean }>>([]); function getImageLoading(id: number) { const loading = imageloading.value.find((a) => a.id === id); if (loading) { return loading.loading; } return false; } return { drinks: computed(() => store.drinks), pagination, columns, column_calc, column_prices, drinkTypes, updateDrink, deleteDrink, showNewDrink, drinkPic, savePicture, deletePicture, console, search, filter, search_keys, tags: computed(() => store.tags), editDrink, editing_drink, get_volumes, notLoading, getImageLoading, newDrink, hasPermission, PERMISSIONS, }; }, }); </script> <style scoped></style>