flaschengeist-pricelist/src/components/CalculationTable.vue

583 lines
15 KiB
Vue

<template>
<q-table
v-model:pagination="pagination"
title="Preistabelle"
:columns="columns"
:rows="drinks"
dense
row-key="id"
grid
:loading="loading"
:filter="search"
@request="onRequest"
>
<!--
:filter-method="filter"
-->
<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="image(props.row.uuid)">
<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>
<template #error>
<q-img class="bg-white" style="max-height: 256px" src="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>
</template>
</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 DrinkPriceVolumes from '../components/CalculationTable/DrinkPriceVolumes.vue';
import { defineComponent, onBeforeMount, ComputedRef, computed, ref } from 'vue';
import { Drink, usePricelistStore, DrinkPriceVolume } from '../store';
import MinPriceSetting from '../components/MinPriceSetting.vue';
import { api, hasPermission } from '@flaschengeist/api';
import { filter, Search } from '../utils/filter';
import SearchInput from './SearchInput.vue';
import DrinkModify from './DrinkModify.vue';
import { DeleteObjects } from '../utils/utils';
import { PERMISSIONS } from '../permissions';
import { sort } from '../utils/sort';
import { Notify } from 'quasar';
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();
onRequest({
pagination: pagination.value,
filter: undefined,
});
});
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: false,
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,
page: 1,
rowsPerPage: 10,
rowsNumber: 10,
});
interface PaginationInterface {
sortBy: string;
descending: boolean;
page: number;
rowsPerPage: number;
rowsNumber: number;
}
const loading = ref(false);
async function onRequest(props: { pagination: PaginationInterface; filter?: Search }) {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
loading.value = true;
console.log('search_keys', search_keys);
const fetchCount = rowsPerPage === 0 ? pagination.value.rowsNumber : rowsPerPage;
const startRow = (page - 1) * rowsPerPage;
try {
const result = await store.getDrinks({
offset: startRow,
limit: fetchCount,
descending,
search_name: props.filter?.value,
search_key: props.filter?.key,
});
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
console.log('result', result.count);
if (result.count) pagination.value.rowsNumber = result.count;
} catch (error) {
console.warn(error);
}
loading.value = false;
}
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);
}
if (deletePic) {
await deletePicture();
}
if (drink.id > 0) {
await store.updateDrink(drink);
} else {
const _drink = await store.setDrink(drink);
if (editDrink.value) {
editDrink.value.id = _drink.id;
}
}
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;
}
function image(uuid: string | undefined) {
if (uuid) {
return `${api.defaults.baseURL || ''}/pricelist/picture/${uuid}?size=256`;
}
return 'no-image.svg';
}
return {
drinks: computed(() => store.drinks),
pagination,
columns,
column_calc,
column_prices,
drinkTypes,
updateDrink,
deleteDrink,
showNewDrink,
drinkPic,
savePicture,
deletePicture,
search,
filter,
search_keys,
tags: computed(() => store.tags),
editDrink,
editing_drink,
get_volumes,
notLoading,
getImageLoading,
newDrink,
hasPermission,
PERMISSIONS,
image,
loading,
onRequest,
};
},
});
</script>
<style scoped></style>