Moved source from main flaschengeist source tree
This commit is contained in:
parent
8e76f49c4d
commit
b1238ed611
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"name": "@flaschengeist/pricelist",
|
||||
"author": "Tim Gröger <flaschengeist@wu5.de>",
|
||||
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||
"description": "Flaschengeist pricelist plugin",
|
||||
"bugs": {
|
||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-pricelist"
|
||||
},
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"pretty": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
||||
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"vuedraggable": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flaschengeist/types": "git+https://flaschengeist.dev/ferfissimo/flaschengeist-types.git#develop",
|
||||
"@quasar/app": "^3.0.0-beta.26",
|
||||
"@typescript-eslint/eslint-plugin": "^4.24.0",
|
||||
"@typescript-eslint/parser": "^4.24.0",
|
||||
"axios": "^0.21.1",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-vue": "^7.9.0",
|
||||
"pinia": "^2.0.0-alpha.18",
|
||||
"prettier": "^2.3.0",
|
||||
"quasar": "^2.0.0-beta.18",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@flaschengeist/api": "^1.0.0-alpha.1",
|
||||
"@flaschengeist/users": "^1.0.0-alpha.1"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 100,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<q-carousel
|
||||
v-model="volume"
|
||||
transition-prev="slide-right"
|
||||
transition-next="slide-left"
|
||||
animated
|
||||
swipeable
|
||||
control-color="primary"
|
||||
arrows
|
||||
>
|
||||
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
|
||||
<build-manual-volume-part :volume="volume" />
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
<div class="full-width row justify-center q-pa-sm">
|
||||
<div class="q-px-sm">
|
||||
<q-btn-toggle v-model="volume" :options="btn_options" rounded />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed } from 'vue';
|
||||
import { DrinkPriceVolume } from '../../store';
|
||||
import BuildManualVolumePart from './BuildManualVolumePart.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BuildManualVolume',
|
||||
components: { BuildManualVolumePart },
|
||||
props: {
|
||||
volumes: {
|
||||
type: Array as PropType<Array<DrinkPriceVolume>>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const _volume = ref<number>();
|
||||
const volume = computed({
|
||||
get: () => {
|
||||
if (_volume.value !== undefined) {
|
||||
return _volume.value;
|
||||
}
|
||||
return props.volumes[0].id;
|
||||
},
|
||||
set: (val: number) => (_volume.value = val),
|
||||
});
|
||||
const options = computed(() => {
|
||||
let ret: Array<{ label: number; value: number }> = [];
|
||||
props.volumes.forEach((volume: DrinkPriceVolume) => {
|
||||
ret.push({ label: volume.id, value: volume.id });
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
const btn_options = computed<Array<{ label: string; value: number }>>(() => {
|
||||
const retVal: Array<{ label: string; value: number }> = [];
|
||||
props.volumes.forEach((volume: DrinkPriceVolume) => {
|
||||
retVal.push({ label: `${(<number>volume.volume).toFixed(3)}L`, value: volume.id });
|
||||
});
|
||||
return retVal;
|
||||
});
|
||||
return { volume, options, btn_options };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Zutaten</div>
|
||||
<div v-for="ingredient in volume.ingredients" :key="ingredient.id">
|
||||
<div v-if="ingredient.drink_ingredient">
|
||||
<div class="full-width row q-gutter-sm q-py-sm">
|
||||
<div class="col">
|
||||
{{ name(ingredient.drink_ingredient?.ingredient_id) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{
|
||||
ingredient.drink_ingredient?.volume
|
||||
? `${ingredient.drink_ingredient?.volume * 100} cl`
|
||||
: ''
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="ingredient in volume.ingredients" :key="ingredient.id">
|
||||
<div v-if="ingredient.extra_ingredient">
|
||||
<div class="full-width row q-gutter-sm q-py-sm">
|
||||
<div class="col">
|
||||
{{ ingredient.extra_ingredient?.name }}
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<q-separator />
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Preise</div>
|
||||
<div class="full-width row q-gutter-sm justify-around">
|
||||
<div v-for="price in volume.prices" :key="price.id">
|
||||
<div class="text-body1">{{ price.price.toFixed(2) }}€</div>
|
||||
<q-badge v-if="price.public" class="text-caption"> öffentlich </q-badge>
|
||||
<div class="text-caption text-weight-thin">
|
||||
{{ price.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { DrinkPriceVolume, usePricelistStore } from '../../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BuildManualVolumePart',
|
||||
props: {
|
||||
volume: {
|
||||
type: Object as PropType<DrinkPriceVolume>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
function name(id: number) {
|
||||
return store.drinks.find((a) => a.id === id)?.name;
|
||||
}
|
||||
return { name };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,511 @@
|
|||
<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="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>
|
||||
</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();
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="full-width row q-gutter-sm justify-between q-py-sm"
|
||||
>
|
||||
<div class="row">
|
||||
<div>{{ index + 1 }}.</div>
|
||||
<div class="q-pl-sm">
|
||||
{{ step }}
|
||||
</div>
|
||||
</div>
|
||||
<q-btn
|
||||
v-if="editable"
|
||||
round
|
||||
color="negative"
|
||||
size="sm"
|
||||
icon="mdi-delete"
|
||||
@click="deleteStep(index)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="editable" class="full-width row q-gutter-sm justify-between">
|
||||
<q-input v-model="newStep" filled label="Arbeitsschritt" dense />
|
||||
<q-btn label="Schritt hinzufügen" dense @click="addStep" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BuildManual',
|
||||
props: {
|
||||
steps: {
|
||||
type: Array as PropType<Array<string>>,
|
||||
default: undefined,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
deleteStep: (index: number) => index,
|
||||
addStep: (val: string) => val,
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
const newStep = ref('');
|
||||
function deleteStep(index: number) {
|
||||
emit('deleteStep', index);
|
||||
}
|
||||
function addStep() {
|
||||
emit('addStep', newStep.value);
|
||||
newStep.value = '';
|
||||
}
|
||||
return { newStep, addStep, deleteStep };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,340 @@
|
|||
<template>
|
||||
<q-carousel
|
||||
v-model="volume"
|
||||
transition-prev="slide-right"
|
||||
transition-next="slide-left"
|
||||
animated
|
||||
swipeable
|
||||
control-color="primary"
|
||||
arrows
|
||||
:keep-alive="false"
|
||||
>
|
||||
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
|
||||
<div class="full-width row">
|
||||
<q-input
|
||||
v-model.number="volume._volume"
|
||||
class="q-pa-sm col-10"
|
||||
:outlined="!editable || !volume_can_edit"
|
||||
:filled="editable && volume_can_edit"
|
||||
:readonly="!editable || !volume_can_edit"
|
||||
dense
|
||||
label="Inhalt"
|
||||
mask="#.###"
|
||||
fill-mask="0"
|
||||
suffix="L"
|
||||
min="0"
|
||||
step="0.001"
|
||||
@update:model-value="updateVolume(volume)"
|
||||
/>
|
||||
<div
|
||||
v-if="deleteable && editable && hasPermission(PERMISSIONS.DELETE_VOLUME)"
|
||||
class="q-pa-sm col-2 text-right"
|
||||
>
|
||||
<q-btn round icon="mdi-delete" size="sm" color="negative" @click="deleteVolume">
|
||||
<q-tooltip> Abgabe entfernen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!public && !nodetails" class="full-width row q-gutter-sm q-pa-sm justify-around">
|
||||
<div v-for="(min_price, index) in volume.min_prices" :key="index">
|
||||
<q-badge class="text-body1" color="primary"> {{ min_price.percentage }}% </q-badge>
|
||||
<div class="text-body1">{{ min_price.price.toFixed(3) }}€</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-pa-sm">
|
||||
<div v-for="(price, index) in volume.prices" :key="price.id">
|
||||
<div class="fit row justify-around q-py-sm">
|
||||
<div
|
||||
v-if="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
|
||||
class="text-body1 col-3"
|
||||
>
|
||||
{{ price.price.toFixed(2) }}€
|
||||
</div>
|
||||
<q-input
|
||||
v-else
|
||||
v-model.number="price.price"
|
||||
class="col-3"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
suffix="€"
|
||||
filled
|
||||
dense
|
||||
label="Preis"
|
||||
@update:model-value="change"
|
||||
/>
|
||||
<div class="text-body1 col-2">
|
||||
<q-toggle
|
||||
v-model="price.public"
|
||||
:disable="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
|
||||
checked-icon="mdi-earth"
|
||||
unchecked-icon="mdi-earth-off"
|
||||
@update:model-value="change"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!editable || !hasPermission(PERMISSIONS.EDIT_PRICE)"
|
||||
class="text-body1 col-5"
|
||||
>
|
||||
{{ price.description }}
|
||||
</div>
|
||||
<q-input
|
||||
v-else
|
||||
v-model="price.description"
|
||||
class="col-5"
|
||||
filled
|
||||
dense
|
||||
label="Beschreibung"
|
||||
@update:model-value="change"
|
||||
/>
|
||||
<div v-if="editable && hasPermission(PERMISSIONS.DELETE_PRICE)" class="col-1">
|
||||
<q-btn round icon="mdi-delete" color="negative" size="xs" @click="deletePrice(price)">
|
||||
<q-tooltip> Preis entfernen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator v-if="index < volume.prices.length - 1" />
|
||||
</div>
|
||||
<div
|
||||
v-if="!public && !nodetails && isUnderMinPrice"
|
||||
class="fit warning bg-red text-center text-white text-body1"
|
||||
>
|
||||
Einer der Preise ist unterhalb des niedrigsten minimal Preises.
|
||||
</div>
|
||||
<div
|
||||
v-if="editable && hasPermission(PERMISSIONS.EDIT_PRICE)"
|
||||
class="full-width row justify-end text-right"
|
||||
>
|
||||
<q-btn round icon="mdi-plus" size="sm" color="primary">
|
||||
<q-tooltip> Preis hinzufügen </q-tooltip>
|
||||
<q-menu anchor="center middle" self="center middle">
|
||||
<new-price @save="addPrice" />
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-pa-sm">
|
||||
<ingredients
|
||||
v-if="!public && !costPerVolume"
|
||||
v-model="volume.ingredients"
|
||||
:editable="editable && hasPermission(PERMISSIONS.EDIT_INGREDIENTS_DRINK)"
|
||||
@update="updateVolume(volume)"
|
||||
@delete-ingredient="deleteIngredient"
|
||||
/>
|
||||
</div>
|
||||
</q-carousel-slide>
|
||||
</q-carousel>
|
||||
<div class="full-width row justify-center q-pa-sm">
|
||||
<div class="q-px-sm">
|
||||
<q-btn-toggle v-model="volume" :options="options" rounded />
|
||||
</div>
|
||||
<div v-if="editable" class="q-px-sm">
|
||||
<q-btn class="q-px-sm" round icon="mdi-plus" color="primary" size="sm" @click="newVolume">
|
||||
<q-tooltip> Abgabe hinzufügen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
|
||||
import { calc_volume, clone } from '../../utils/utils';
|
||||
import { hasPermission } from '@flaschengeist/api';
|
||||
import { PERMISSIONS } from '../../permissions';
|
||||
import { DrinkPriceVolume } from '../../store';
|
||||
import Ingredients from './Ingredients.vue';
|
||||
import NewPrice from './NewPrice.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DrinkPriceVolume',
|
||||
components: { Ingredients, NewPrice },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Array<DrinkPriceVolume>>,
|
||||
required: true,
|
||||
},
|
||||
costPerVolume: {
|
||||
type: undefined,
|
||||
default: undefined,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
public: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
nodetails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:modelValue': (val: Array<DrinkPriceVolume>) => val,
|
||||
update: (val: number) => val,
|
||||
'delete-volume': (val: DrinkPriceVolume) => val,
|
||||
'delete-price': (val: FG.DrinkPrice) => val,
|
||||
'delete-ingredient': (val: FG.Ingredient) => val,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
onBeforeMount(() => {
|
||||
//volumes.value = <Array<DrinkPriceVolume>>JSON.parse(JSON.stringify(props.modelValue));
|
||||
volumes.value = clone(props.modelValue);
|
||||
});
|
||||
const volumes = ref<Array<DrinkPriceVolume>>([]);
|
||||
const _volume = ref<number | undefined>();
|
||||
const volume = computed<number | undefined>({
|
||||
get: () => {
|
||||
if (_volume.value !== undefined) {
|
||||
return _volume.value;
|
||||
}
|
||||
if (volumes.value.length > 0) {
|
||||
return volumes.value[0].id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
set: (val: number | undefined) => (_volume.value = val),
|
||||
});
|
||||
const edit_volume = computed(() => {
|
||||
return volumes.value.find((a) => a.id === volume.value);
|
||||
});
|
||||
const options = computed<Array<{ label: string; value: number }>>(() => {
|
||||
const retVal: Array<{ label: string; value: number }> = [];
|
||||
volumes.value.forEach((volume: DrinkPriceVolume) => {
|
||||
retVal.push({ label: `${(<number>volume.volume).toFixed(3)}L`, value: volume.id });
|
||||
});
|
||||
return retVal;
|
||||
});
|
||||
|
||||
function updateVolume(_volume: DrinkPriceVolume) {
|
||||
const index = volumes.value.findIndex((a) => a.id === _volume.id);
|
||||
if (index > -1) {
|
||||
volumes.value[index].volume = calc_volume(_volume);
|
||||
volumes.value[index]._volume = <number>volumes.value[index].volume;
|
||||
}
|
||||
change();
|
||||
setTimeout(() => {
|
||||
emit('update', index);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
const volume_can_edit = computed(() => {
|
||||
if (edit_volume.value) {
|
||||
return !edit_volume.value.ingredients.some((ingredient) => ingredient.drink_ingredient);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const newVolumeId = ref(-1);
|
||||
|
||||
function newVolume() {
|
||||
const new_volume: DrinkPriceVolume = {
|
||||
id: newVolumeId.value,
|
||||
_volume: 0,
|
||||
volume: 0,
|
||||
prices: [],
|
||||
ingredients: [],
|
||||
min_prices: [],
|
||||
};
|
||||
newVolumeId.value--;
|
||||
volumes.value.push(new_volume);
|
||||
change();
|
||||
_volume.value = volumes.value[volumes.value.length - 1].id;
|
||||
}
|
||||
|
||||
function deleteVolume() {
|
||||
if (edit_volume.value) {
|
||||
if (edit_volume.value.id > 0) {
|
||||
emit('delete-volume', edit_volume.value);
|
||||
}
|
||||
const index = volumes.value.findIndex((a) => a.id === edit_volume.value?.id);
|
||||
if (index > -1) {
|
||||
_volume.value = volumes.value[0].id;
|
||||
volumes.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteable = computed(() => {
|
||||
if (edit_volume.value) {
|
||||
const has_ingredients = edit_volume.value.ingredients.length > 0;
|
||||
const has_prices = edit_volume.value.prices.length > 0;
|
||||
|
||||
return !(has_ingredients || has_prices);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function addPrice(price: FG.DrinkPrice) {
|
||||
if (edit_volume.value) {
|
||||
edit_volume.value.prices.push(price);
|
||||
change();
|
||||
}
|
||||
}
|
||||
|
||||
function deletePrice(price: FG.DrinkPrice) {
|
||||
if (edit_volume.value) {
|
||||
const index = edit_volume.value.prices.findIndex((a) => a.id === price.id);
|
||||
if (index > -1) {
|
||||
if (edit_volume.value.prices[index].id > 0) {
|
||||
emit('delete-price', edit_volume.value.prices[index]);
|
||||
change();
|
||||
}
|
||||
edit_volume.value.prices.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deleteIngredient(ingredient: FG.Ingredient) {
|
||||
emit('delete-ingredient', ingredient);
|
||||
}
|
||||
|
||||
function change() {
|
||||
emit('update:modelValue', volumes.value);
|
||||
}
|
||||
|
||||
const isUnderMinPrice = computed(() => {
|
||||
if (volumes.value) {
|
||||
const this_volume = volumes.value.find((a) => a.id === volume.value);
|
||||
if (this_volume) {
|
||||
if (this_volume.min_prices.length > 0) {
|
||||
const min_price = this_volume.min_prices.sort((a, b) => {
|
||||
if (a.price > b.price) return 1;
|
||||
if (a.price < b.price) return -1;
|
||||
return 0;
|
||||
})[0];
|
||||
console.log('min_price', min_price);
|
||||
return this_volume.prices.some((a) => a.price < min_price.price);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
volumes,
|
||||
volume,
|
||||
options,
|
||||
updateVolume,
|
||||
volume_can_edit,
|
||||
newVolume,
|
||||
deleteable,
|
||||
addPrice,
|
||||
deletePrice,
|
||||
deleteVolume,
|
||||
deleteIngredient,
|
||||
change,
|
||||
isUnderMinPrice,
|
||||
hasPermission,
|
||||
PERMISSIONS,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.warning {
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,271 @@
|
|||
<template>
|
||||
<div class="full-width">
|
||||
<div
|
||||
v-for="ingredient in edit_ingredients"
|
||||
:key="`ingredient:${ingredient.id}`"
|
||||
class="full-width row justify-evenly q-py-xs"
|
||||
>
|
||||
<div class="full-width row justify-evenly">
|
||||
<div v-if="ingredient.drink_ingredient" class="col">
|
||||
<div class="full-width row justify-evenly q-py-xs">
|
||||
<div class="col">
|
||||
{{ get_drink_ingredient_name(ingredient.drink_ingredient.ingredient_id) }}
|
||||
<q-popup-edit
|
||||
v-if="editable"
|
||||
v-slot="scope"
|
||||
v-model="ingredient.drink_ingredient.ingredient_id"
|
||||
buttons
|
||||
label-cancel="Abbrechen"
|
||||
label-set="Speichern"
|
||||
@save="updateValue"
|
||||
>
|
||||
<q-select
|
||||
v-model="scope.ingredient_id"
|
||||
class="col q-px-sm"
|
||||
label="Getränk"
|
||||
filled
|
||||
dense
|
||||
:options="drinks"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-popup-edit>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ ingredient.drink_ingredient.volume.toFixed(3) }}L
|
||||
<q-popup-edit
|
||||
v-if="editable"
|
||||
v-slot="scope"
|
||||
v-model="ingredient.drink_ingredient.volume"
|
||||
buttons
|
||||
label-cancel="Abbrechen"
|
||||
label-set="Speichern"
|
||||
@save="updateValue"
|
||||
>
|
||||
<q-input
|
||||
v-model.number="scope.value"
|
||||
class="col q-px-sm"
|
||||
label="Volume"
|
||||
type="number"
|
||||
filled
|
||||
dense
|
||||
suffix="L"
|
||||
step="0.01"
|
||||
min="0"
|
||||
/>
|
||||
</q-popup-edit>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="ingredient.extra_ingredient" class="col">
|
||||
<div class="full-width row justify-evenly q-py-xs">
|
||||
<div class="col">
|
||||
{{ ingredient.extra_ingredient.name }}
|
||||
</div>
|
||||
<div class="col">{{ ingredient.extra_ingredient.price.toFixed(3) }}€</div>
|
||||
</div>
|
||||
<q-popup-edit
|
||||
v-if="editable"
|
||||
v-model="ingredient.extra_ingredient"
|
||||
buttons
|
||||
label-cancel="Abbrechen"
|
||||
label-set="Speichern"
|
||||
@save="updateValue"
|
||||
>
|
||||
<q-select
|
||||
v-model="ingredient.extra_ingredient"
|
||||
filled
|
||||
dense
|
||||
:options="extra_ingredients"
|
||||
option-label="name"
|
||||
/>
|
||||
</q-popup-edit>
|
||||
</div>
|
||||
<div v-if="editable" class="col-1 row justify-end q-pa-xs">
|
||||
<q-btn
|
||||
icon="mdi-delete"
|
||||
round
|
||||
size="xs"
|
||||
color="negative"
|
||||
@click="deleteIngredient(ingredient)"
|
||||
>
|
||||
<q-tooltip> Zutat entfernen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
</div>
|
||||
<div v-if="editable" class="full-width row justify-end q-py-xs">
|
||||
<q-btn size="sm" round icon="mdi-plus" color="primary">
|
||||
<q-tooltip> Neue Zutat hinzufügen </q-tooltip>
|
||||
<q-menu anchor="center middle" self="center middle" persistent>
|
||||
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="newIngredient"
|
||||
filled
|
||||
dense
|
||||
label="Zutat"
|
||||
:options="[...drinks, ...extra_ingredients]"
|
||||
option-label="name"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-slide-transition>
|
||||
<q-input
|
||||
v-if="newIngredient && newIngredient.volume"
|
||||
v-model.number="newIngredientVolume"
|
||||
filled
|
||||
dense
|
||||
label="Volume"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
suffix="L"
|
||||
/>
|
||||
</q-slide-transition>
|
||||
<q-slide-transition>
|
||||
<q-input
|
||||
v-if="newIngredient && newIngredient.price"
|
||||
v-model="newIngredient.price"
|
||||
filled
|
||||
dense
|
||||
label="Preis"
|
||||
disable
|
||||
min="0"
|
||||
step="0.1"
|
||||
fill-mask="0"
|
||||
mask="#.##"
|
||||
suffix="€"
|
||||
/>
|
||||
</q-slide-transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
|
||||
<q-btn v-close-popup flat label="Abbrechen" @click="cancelAddIngredient" />
|
||||
<q-btn v-close-popup flat label="Speichern" color="primary" @click="addIngredient" />
|
||||
</div>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, ref, onBeforeMount, unref } from 'vue';
|
||||
import { usePricelistStore } from '../../store';
|
||||
import { clone } from '../../utils/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Ingredients',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as PropType<Array<FG.Ingredient>>,
|
||||
required: true,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:modelValue': (val: Array<FG.Ingredient>) => val,
|
||||
update: () => true,
|
||||
'delete-ingredient': (val: FG.Ingredient) => val,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
onBeforeMount(() => {
|
||||
//edit_ingredients.value = <Array<FG.Ingredient>>JSON.parse(JSON.stringify(props.modelValue))
|
||||
//edit_ingredients.value = props.modelValue
|
||||
edit_ingredients.value = clone(props.modelValue);
|
||||
});
|
||||
|
||||
const store = usePricelistStore();
|
||||
|
||||
const edit_ingredients = ref<Array<FG.Ingredient>>([]);
|
||||
const newIngredient = ref<FG.Drink | FG.ExtraIngredient>();
|
||||
const newIngredientVolume = ref<number>(0);
|
||||
function addIngredient() {
|
||||
let _ingredient: FG.Ingredient;
|
||||
if ((<FG.Drink>newIngredient.value)?.volume && newIngredient.value) {
|
||||
_ingredient = {
|
||||
id: -1,
|
||||
drink_ingredient: {
|
||||
id: -1,
|
||||
ingredient_id: newIngredient.value.id,
|
||||
volume: newIngredientVolume.value,
|
||||
},
|
||||
extra_ingredient: undefined,
|
||||
};
|
||||
} else {
|
||||
_ingredient = {
|
||||
id: -1,
|
||||
drink_ingredient: undefined,
|
||||
extra_ingredient: <FG.ExtraIngredient>newIngredient.value,
|
||||
};
|
||||
}
|
||||
edit_ingredients.value.push(_ingredient);
|
||||
emit('update:modelValue', unref(edit_ingredients));
|
||||
update();
|
||||
cancelAddIngredient();
|
||||
}
|
||||
|
||||
function updateValue(value: number, initValue: number) {
|
||||
console.log('updateValue', value, initValue);
|
||||
emit('update:modelValue', unref(edit_ingredients));
|
||||
update();
|
||||
}
|
||||
|
||||
function cancelAddIngredient() {
|
||||
setTimeout(() => {
|
||||
(newIngredient.value = undefined), (newIngredientVolume.value = 0);
|
||||
}, 200);
|
||||
}
|
||||
function deleteIngredient(ingredient: FG.Ingredient) {
|
||||
const index = edit_ingredients.value.findIndex((a) => a.id === ingredient.id);
|
||||
if (index > -1) {
|
||||
if (edit_ingredients.value[index].id > 0) {
|
||||
emit('delete-ingredient', edit_ingredients.value[index]);
|
||||
}
|
||||
edit_ingredients.value.splice(index, 1);
|
||||
}
|
||||
emit('update:modelValue', unref(edit_ingredients));
|
||||
update();
|
||||
}
|
||||
const drinks = computed(() =>
|
||||
store.drinks.filter((drink) => {
|
||||
console.log('computed drinks', drink.name, drink.cost_per_volume);
|
||||
return drink.cost_per_volume;
|
||||
})
|
||||
);
|
||||
const extra_ingredients = computed(() => store.extraIngredients);
|
||||
|
||||
function get_drink_ingredient_name(id: number) {
|
||||
return store.drinks.find((a) => a.id === id)?.name;
|
||||
}
|
||||
|
||||
function update() {
|
||||
setTimeout(() => {
|
||||
emit('update');
|
||||
}, 50);
|
||||
}
|
||||
|
||||
return {
|
||||
addIngredient,
|
||||
drinks,
|
||||
extra_ingredients,
|
||||
newIngredient,
|
||||
newIngredientVolume,
|
||||
cancelAddIngredient,
|
||||
updateValue,
|
||||
deleteIngredient,
|
||||
get_drink_ingredient_name,
|
||||
edit_ingredients,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div class="row justify-around q-pa-sm">
|
||||
<q-input
|
||||
v-model.number="newPrice.price"
|
||||
dense
|
||||
filled
|
||||
class="q-px-sm"
|
||||
type="number"
|
||||
label="Preis"
|
||||
suffix="€"
|
||||
min="0"
|
||||
step="0.1"
|
||||
/>
|
||||
<q-input
|
||||
v-model="newPrice.description"
|
||||
dense
|
||||
filled
|
||||
class="q-px-sm"
|
||||
label="Beschreibung"
|
||||
clearable
|
||||
/>
|
||||
<q-toggle v-model="newPrice.public" dense class="q-px-sm" label="Öffentlich" />
|
||||
</div>
|
||||
<div class="row justify-between q-pa-sm">
|
||||
<q-btn v-close-popup label="Abbrechen" @click="cancelAddPrice" />
|
||||
<q-btn v-close-popup label="Speichern" color="primary" @click="addPrice(row)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NewPrice',
|
||||
emits: {
|
||||
save: (val: FG.DrinkPrice) => val,
|
||||
},
|
||||
setup(_, { emit }) {
|
||||
const emptyPrice: FG.DrinkPrice = {
|
||||
id: -1,
|
||||
price: 0,
|
||||
description: '',
|
||||
public: true,
|
||||
};
|
||||
const newPrice = ref(emptyPrice);
|
||||
function addPrice() {
|
||||
emit('save', newPrice.value);
|
||||
cancelAddPrice();
|
||||
}
|
||||
function cancelAddPrice() {
|
||||
setTimeout(() => {
|
||||
newPrice.value = emptyPrice;
|
||||
}, 200);
|
||||
}
|
||||
return { newPrice, addPrice, cancelAddPrice };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,353 @@
|
|||
<template>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Getränk Bearbeiten</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="full-width row">
|
||||
<q-input
|
||||
v-model="edit_drink.name"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
filled
|
||||
label="Name"
|
||||
dense
|
||||
/>
|
||||
<q-select
|
||||
v-model="edit_drink.type"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
filled
|
||||
label="Kategorie"
|
||||
dense
|
||||
:options="types"
|
||||
option-label="name"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-img :src="image" style="max-height: 256px" fit="contain" />
|
||||
<div class="full-width row">
|
||||
<div class="col-10 q-pa-sm">
|
||||
<q-file
|
||||
v-model="drinkPic"
|
||||
filled
|
||||
clearable
|
||||
dense
|
||||
@update:model-value="imagePreview"
|
||||
@clear="imgsrc = undefined"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="mdi-image" />
|
||||
</template>
|
||||
</q-file>
|
||||
</div>
|
||||
<div class="col-2 q-pa-sm text-right">
|
||||
<q-btn round icon="mdi-delete" color="negative" size="sm" @click="delete_pic">
|
||||
<q-tooltip> Bild entfernen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-select
|
||||
v-model="edit_drink.tags"
|
||||
multiple
|
||||
:options="tags"
|
||||
label="Tags"
|
||||
option-label="name"
|
||||
filled
|
||||
dense
|
||||
>
|
||||
<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>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="fit row">
|
||||
<q-input
|
||||
v-model="edit_drink.article_id"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
filled
|
||||
label="Artikelnummer"
|
||||
dense
|
||||
/>
|
||||
<q-input
|
||||
v-model="edit_drink.volume"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
filled
|
||||
label="Inhalt"
|
||||
dense
|
||||
suffix="L"
|
||||
/>
|
||||
<q-input
|
||||
v-model="edit_drink.package_size"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
filled
|
||||
label="Gebindegröße"
|
||||
dense
|
||||
/>
|
||||
<q-input
|
||||
v-model="edit_drink.cost_per_package"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
filled
|
||||
label="Preis Gebinde"
|
||||
suffix="€"
|
||||
dense
|
||||
/>
|
||||
<q-input
|
||||
v-model="cost_per_volume"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg"
|
||||
:outlined="auto_cost_per_volume || hasIngredients"
|
||||
:filled="!auto_cost_per_volume && !hasIngredients"
|
||||
:readonly="auto_cost_per_volume || hasIngredients"
|
||||
label="Preis pro L"
|
||||
hint="Inkl. 19% Mehrwertsteuer"
|
||||
suffix="€"
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section :key="key">
|
||||
<drink-price-volumes
|
||||
v-model="edit_volumes"
|
||||
:cost-per-volume="cost_per_volume"
|
||||
:editable="hasPermission(PERMISSIONS.EDIT_VOLUME)"
|
||||
@update="updateVolume"
|
||||
@delete-volume="deleteVolume"
|
||||
@delete-price="deletePrice"
|
||||
@delete-ingredient="deleteIngredient"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<build-manual :steps="edit_drink.receipt" @deleteStep="deleteStep" @addStep="addStep" />
|
||||
</q-card-section>
|
||||
<q-card-actions class="justify-around">
|
||||
<q-btn label="Abbrechen" @click="cancel" />
|
||||
<q-btn v-if="can_delete" label="Löschen" color="negative" @click="delete_drink" />
|
||||
<q-btn label="Speichern" color="primary" @click="save" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { clone, calc_min_prices, DeleteObjects, calc_cost_per_volume } from '../utils/utils';
|
||||
import { defineComponent, PropType, ref, onBeforeMount, computed } from 'vue';
|
||||
import DrinkPriceVolumes from './CalculationTable/DrinkPriceVolumes.vue';
|
||||
import BuildManual from '../components/CalculationTable/BuildManual.vue';
|
||||
import { Drink, DrinkPriceVolume, usePricelistStore } from '../store';
|
||||
import { api, hasPermission } from '@flaschengeist/api';
|
||||
import { PERMISSIONS } from '../permissions';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DrinkModify',
|
||||
components: { BuildManual, DrinkPriceVolumes },
|
||||
props: {
|
||||
drink: {
|
||||
type: Object as PropType<Drink>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
save: (
|
||||
drink: Drink,
|
||||
toDeleteObjects: DeleteObjects,
|
||||
drinkPic: File | undefined,
|
||||
deletePic: boolean
|
||||
) => (drink && toDeleteObjects) || drinkPic || deletePic,
|
||||
delete: () => true,
|
||||
cancel: () => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
onBeforeMount(() => {
|
||||
//edit_drink.value = <Drink>JSON.parse(JSON.stringify(props.drink));
|
||||
edit_drink.value = clone(props.drink);
|
||||
edit_volumes.value = clone(props.drink.volumes);
|
||||
});
|
||||
|
||||
const key = ref(0);
|
||||
|
||||
const store = usePricelistStore();
|
||||
|
||||
const toDeleteObjects = ref<DeleteObjects>({
|
||||
prices: [],
|
||||
volumes: [],
|
||||
ingredients: [],
|
||||
});
|
||||
|
||||
const edit_drink = ref<Drink>();
|
||||
const edit_volumes = ref<Array<DrinkPriceVolume>>([]);
|
||||
function save() {
|
||||
(<Drink>edit_drink.value).volumes = edit_volumes.value;
|
||||
emit('save', <Drink>edit_drink.value, toDeleteObjects.value, drinkPic.value, deletePic.value);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
function updateVolume(index: number) {
|
||||
if (index > -1 && edit_volumes.value) {
|
||||
edit_volumes.value[index].min_prices = calc_min_prices(
|
||||
edit_volumes.value[index],
|
||||
//edit_drink.value.cost_per_volume,
|
||||
cost_per_volume.value,
|
||||
store.min_prices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateVolumes() {
|
||||
setTimeout(() => {
|
||||
edit_volumes.value?.forEach((_, index) => {
|
||||
updateVolume(index);
|
||||
});
|
||||
key.value++;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function deletePrice(price: FG.DrinkPrice) {
|
||||
toDeleteObjects.value.prices.push(price);
|
||||
}
|
||||
|
||||
function deleteVolume(volume: DrinkPriceVolume) {
|
||||
toDeleteObjects.value.volumes.push(volume);
|
||||
}
|
||||
|
||||
function deleteIngredient(ingredient: FG.Ingredient) {
|
||||
toDeleteObjects.value.ingredients.push(ingredient);
|
||||
}
|
||||
|
||||
function addStep(event: string) {
|
||||
edit_drink.value?.receipt?.push(event);
|
||||
}
|
||||
|
||||
function deleteStep(event: number) {
|
||||
edit_drink.value?.receipt?.splice(event, 1);
|
||||
}
|
||||
|
||||
const drinkPic = ref();
|
||||
const imgsrc = ref();
|
||||
|
||||
const deletePic = ref(false);
|
||||
|
||||
function delete_pic() {
|
||||
deletePic.value = true;
|
||||
imgsrc.value = undefined;
|
||||
drinkPic.value = undefined;
|
||||
if (edit_drink.value) {
|
||||
edit_drink.value.uuid = '';
|
||||
}
|
||||
}
|
||||
|
||||
function imagePreview() {
|
||||
if (drinkPic.value && drinkPic.value instanceof File) {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
imgsrc.value = e.target?.result;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(drinkPic.value);
|
||||
}
|
||||
}
|
||||
|
||||
const image = computed(() => {
|
||||
if (deletePic.value) {
|
||||
return 'no-image.svg';
|
||||
}
|
||||
if (imgsrc.value) {
|
||||
return <string>imgsrc.value;
|
||||
}
|
||||
if (edit_drink.value?.uuid) {
|
||||
return `${api.defaults.baseURL||''}/pricelist/picture/${edit_drink.value.uuid}?size=256`;
|
||||
}
|
||||
return 'no-image.svg';
|
||||
});
|
||||
|
||||
const can_delete = computed(() => {
|
||||
if (edit_drink.value) {
|
||||
if (edit_drink.value.id < 0) {
|
||||
return false;
|
||||
}
|
||||
const _edit_drink = edit_drink.value;
|
||||
const test = _edit_drink.volumes ? _edit_drink.volumes.length === 0 : true;
|
||||
console.log(test);
|
||||
return test;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
function delete_drink() {
|
||||
emit('delete');
|
||||
}
|
||||
|
||||
const auto_cost_per_volume = computed(
|
||||
() =>
|
||||
!!(
|
||||
edit_drink.value?.cost_per_package &&
|
||||
edit_drink.value?.package_size &&
|
||||
edit_drink.value?.volume
|
||||
)
|
||||
);
|
||||
|
||||
const cost_per_volume = computed({
|
||||
get: () => {
|
||||
let retVal: number;
|
||||
if (auto_cost_per_volume.value) {
|
||||
retVal = <number>calc_cost_per_volume(<Drink>edit_drink.value);
|
||||
} else {
|
||||
retVal = <number>(<Drink>edit_drink.value).cost_per_volume;
|
||||
}
|
||||
updateVolumes();
|
||||
return retVal;
|
||||
},
|
||||
set: (val: number) => ((<Drink>edit_drink.value).cost_per_volume = val),
|
||||
});
|
||||
|
||||
const hasIngredients = computed(() =>
|
||||
edit_volumes.value?.some((a) => a.ingredients.length > 0)
|
||||
);
|
||||
|
||||
return {
|
||||
edit_drink,
|
||||
save,
|
||||
cancel,
|
||||
updateVolume,
|
||||
deletePrice,
|
||||
deleteIngredient,
|
||||
deleteVolume,
|
||||
addStep,
|
||||
deleteStep,
|
||||
tags: computed(() => store.tags),
|
||||
image,
|
||||
imgsrc,
|
||||
drinkPic,
|
||||
imagePreview,
|
||||
delete_pic,
|
||||
types: computed(() => store.drinkTypes),
|
||||
can_delete,
|
||||
delete_drink,
|
||||
auto_cost_per_volume,
|
||||
cost_per_volume,
|
||||
edit_volumes,
|
||||
key,
|
||||
hasIngredients,
|
||||
hasPermission,
|
||||
PERMISSIONS,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<q-table
|
||||
title="Getränkearten"
|
||||
:rows="rows"
|
||||
:row-key="(row) => row.id"
|
||||
:columns="columns"
|
||||
style="height: 100%"
|
||||
:pagination="pagination"
|
||||
>
|
||||
<template #top-right>
|
||||
<div class="full-width row q-gutter-sm">
|
||||
<q-input v-model="newDrinkType" dense placeholder="Neue Getränkeart" filled />
|
||||
<q-btn round color="primary" icon="mdi-plus" @click="addType">
|
||||
<q-tooltip> Getränkeart hinzufügen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
<template #body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="drinkTypeName" :props="props">
|
||||
{{ props.row.name }}
|
||||
<q-popup-edit
|
||||
v-model="props.row.name"
|
||||
buttons
|
||||
label-set="Speichern"
|
||||
label-cancel="Abbrechen"
|
||||
@save="saveChanges(props.row)"
|
||||
>
|
||||
<template #default="scope">
|
||||
<q-input v-model="scope.value" dense label="name" filled />
|
||||
</template>
|
||||
</q-popup-edit>
|
||||
</q-td>
|
||||
<q-td key="actions" :props="props">
|
||||
<q-btn
|
||||
round
|
||||
icon="mdi-delete"
|
||||
color="negative"
|
||||
size="sm"
|
||||
@click="deleteType(props.row.id)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
||||
import { usePricelistStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DrinkTypes',
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
const newDrinkType = ref('');
|
||||
|
||||
onBeforeMount(() => {
|
||||
void store.getDrinkTypes(true);
|
||||
});
|
||||
const rows = computed(() => store.drinkTypes);
|
||||
const columns = [
|
||||
{
|
||||
name: 'drinkTypeName',
|
||||
label: 'Getränkeart',
|
||||
field: 'name',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: 'Aktionen',
|
||||
field: 'actions',
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
async function addType() {
|
||||
await store.addDrinkType(newDrinkType.value);
|
||||
newDrinkType.value = '';
|
||||
}
|
||||
|
||||
function saveChanges(drinkType: FG.DrinkType) {
|
||||
setTimeout(() => {
|
||||
const _drinkType = store.drinkTypes.find((a) => a.id === drinkType.id);
|
||||
if (_drinkType) {
|
||||
void store.changeDrinkTypeName(drinkType);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function deleteType(id: number) {
|
||||
void store.removeDrinkType(id);
|
||||
}
|
||||
const pagination = ref({
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 10,
|
||||
});
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
addType,
|
||||
newDrinkType,
|
||||
deleteType,
|
||||
saveChanges,
|
||||
pagination,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<div>
|
||||
<q-page padding>
|
||||
<q-table
|
||||
title="Getränkearten"
|
||||
:rows="rows"
|
||||
:row-key="(row) => row.id"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
>
|
||||
<template #top-right>
|
||||
<div class="full-width row q-gutter-sm">
|
||||
<q-input
|
||||
v-model="newExtraIngredient.name"
|
||||
dense
|
||||
placeholder="Neue Zutatenbezeichnung"
|
||||
label="Neue Zutatenbezeichnung"
|
||||
filled
|
||||
/>
|
||||
<q-input
|
||||
v-model.number="newExtraIngredient.price"
|
||||
dense
|
||||
placeholder="Preis"
|
||||
label="Preis"
|
||||
filled
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
suffix="€"
|
||||
/>
|
||||
<q-btn color="primary" icon="mdi-plus" round @click="addExtraIngredient">
|
||||
<q-tooltip> Zutat hinzufügen </q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
<template #body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="name" :props="props" align="left">
|
||||
{{ props.row.name }}
|
||||
<q-popup-edit
|
||||
v-model="props.row.name"
|
||||
buttons
|
||||
label-set="Speichern"
|
||||
label-cancel="Abbrechen"
|
||||
@save="saveChanges(props.row)"
|
||||
>
|
||||
<template #default="scope">
|
||||
<q-input v-model="scope.value" dense label="name" filled />
|
||||
</template>
|
||||
</q-popup-edit>
|
||||
</q-td>
|
||||
<q-td key="price" :props="props" align="right">
|
||||
{{ props.row.price.toFixed(2) }}€
|
||||
</q-td>
|
||||
<q-td key="actions" :props="props" align="right" auto-width>
|
||||
<q-btn
|
||||
round
|
||||
icon="mdi-delete"
|
||||
color="negative"
|
||||
size="sm"
|
||||
@click="deleteType(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ComputedRef, defineComponent, ref } from 'vue';
|
||||
import { usePricelistStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DrinkTypes',
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
const emptyExtraIngredient: FG.ExtraIngredient = {
|
||||
name: '',
|
||||
price: 0,
|
||||
id: -1,
|
||||
};
|
||||
const newExtraIngredient = ref<FG.ExtraIngredient>(emptyExtraIngredient);
|
||||
|
||||
const rows = computed(() => store.extraIngredients);
|
||||
const columns = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Bezeichnung',
|
||||
field: 'name',
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Preis',
|
||||
field: 'price',
|
||||
sortable: true,
|
||||
format: (val: number) => `${val.toFixed(2)}€`,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: 'Aktionen',
|
||||
field: 'actions',
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
async function addExtraIngredient() {
|
||||
await store.setExtraIngredient((<ComputedRef>newExtraIngredient).value);
|
||||
newExtraIngredient.value = Object.assign({}, emptyExtraIngredient);
|
||||
discardChanges();
|
||||
}
|
||||
|
||||
function saveChanges(ingredient: FG.ExtraIngredient) {
|
||||
setTimeout(() => {
|
||||
const _ingredient = store.extraIngredients.find((a) => a.id === ingredient.id);
|
||||
if (_ingredient) {
|
||||
void store.updateExtraIngredient(_ingredient);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function discardChanges() {
|
||||
newExtraIngredient.value.name = '';
|
||||
newExtraIngredient.value.price = 0;
|
||||
}
|
||||
|
||||
function deleteType(extraIngredient: FG.ExtraIngredient) {
|
||||
void store.deleteExtraIngredient(extraIngredient);
|
||||
}
|
||||
|
||||
const pagination = ref({
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 10,
|
||||
});
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
addExtraIngredient,
|
||||
newExtraIngredient,
|
||||
deleteType,
|
||||
discardChanges,
|
||||
saveChanges,
|
||||
pagination,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<q-list>
|
||||
<div v-for="(min_price, index) in min_prices" :key="index">
|
||||
<q-item>
|
||||
<q-item-section>{{ min_price }}%</q-item-section>
|
||||
<q-btn
|
||||
round
|
||||
icon="mdi-delete"
|
||||
size="sm"
|
||||
color="negative"
|
||||
@click="delete_min_price(min_price)"
|
||||
/>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
</div>
|
||||
</q-list>
|
||||
<q-input v-model.number="new_min_price" class="q-pa-sm" type="number" suffix="%" filled dense />
|
||||
<q-btn class="full-width" label="speichern" @click="save_min_prices"></q-btn>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import { usePricelistStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MinPriceSetting',
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
const min_prices = computed(() => store.min_prices);
|
||||
const new_min_price = ref<number>();
|
||||
function save_min_prices() {
|
||||
const index = min_prices.value.findIndex((a) => a === new_min_price.value);
|
||||
if (index < 0) {
|
||||
min_prices.value.push(<number>new_min_price.value);
|
||||
void store.set_min_prices();
|
||||
new_min_price.value = undefined;
|
||||
}
|
||||
}
|
||||
function delete_min_price(min_price: number) {
|
||||
const index = min_prices.value.findIndex((a) => a === min_price);
|
||||
if (index > -1) {
|
||||
min_prices.value.splice(index, 1);
|
||||
void store.set_min_prices();
|
||||
}
|
||||
}
|
||||
return {
|
||||
min_prices,
|
||||
new_min_price,
|
||||
save_min_prices,
|
||||
delete_min_price,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,326 @@
|
|||
<template>
|
||||
<q-table
|
||||
title="Preisliste"
|
||||
:columns="columns"
|
||||
:rows="drinks"
|
||||
:visible-columns="visibleColumns"
|
||||
:filter="search"
|
||||
:filter-method="filter"
|
||||
dense
|
||||
:pagination="pagination"
|
||||
:fullscreen="fullscreen"
|
||||
>
|
||||
<template #top-right>
|
||||
<div class="row justify-end q-gutter-sm">
|
||||
<search-input v-model="search" :keys="options" />
|
||||
<q-select
|
||||
v-model="visibleColumns"
|
||||
multiple
|
||||
filled
|
||||
dense
|
||||
options-dense
|
||||
display-value="Sichtbarkeit"
|
||||
emit-value
|
||||
map-options
|
||||
:options="options"
|
||||
option-value="name"
|
||||
options-cover
|
||||
/>
|
||||
<q-btn round icon="mdi-backburger">
|
||||
<q-tooltip anchor='top middle' self='bottom middle'> Reihenfolge ändern </q-tooltip>
|
||||
<q-menu anchor="bottom middle" self="top middle">
|
||||
<drag v-model="order" class="q-list" ghost-class="ghost" group="people" item-key="id">
|
||||
<template #item="{ element }">
|
||||
<q-item>
|
||||
<q-item-section>
|
||||
{{ element.label }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</drag>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<slot></slot>
|
||||
<q-btn
|
||||
round
|
||||
:icon="fullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
|
||||
@click="fullscreen = !fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-cell-tags="props">
|
||||
<q-td :props="props">
|
||||
<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-td>
|
||||
</template>
|
||||
<template #body-cell-public="props">
|
||||
<q-td :props="props">
|
||||
<q-toggle
|
||||
v-model="props.row.public"
|
||||
disable
|
||||
checked-icon="mdi-earth"
|
||||
unchecked-icon="mdi-earth-off"
|
||||
/>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeMount, ref, ComponentPublicInstance } from 'vue';
|
||||
import SearchInput from '../components/SearchInput.vue';
|
||||
import { usePricelistStore, Order } from '../store';
|
||||
import { useMainStore } from '@flaschengeist/api';
|
||||
import { Search, filter } from '../utils/filter';
|
||||
import drag from 'vuedraggable';
|
||||
|
||||
interface Row {
|
||||
name: string;
|
||||
label: string;
|
||||
field: string;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
format?: (val: never) => string;
|
||||
align?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Pricelist',
|
||||
components: { SearchInput, drag: <ComponentPublicInstance>drag },
|
||||
props: {
|
||||
public: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = usePricelistStore();
|
||||
const user = ref('');
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!props.public) {
|
||||
user.value = useMainStore().currentUser.userid;
|
||||
void store.getPriceListColumnOrder(user.value);
|
||||
void store.getDrinks();
|
||||
void store.getPriceCalcColumn(user.value);
|
||||
} else {
|
||||
user.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
const _order = ref<Array<Order>>([
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Kategorie',
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
},
|
||||
{
|
||||
name: 'volume',
|
||||
label: 'Inhalt',
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Preis',
|
||||
},
|
||||
{
|
||||
name: 'public',
|
||||
label: 'Öffentlich',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Beschreibung',
|
||||
},
|
||||
]);
|
||||
|
||||
const order = computed<Array<Order>>({
|
||||
get: () => {
|
||||
if (props.public) {
|
||||
return _order.value;
|
||||
}
|
||||
if (store.pricelist_columns_order.length === 0) {
|
||||
return _order.value;
|
||||
}
|
||||
return store.pricelist_columns_order;
|
||||
},
|
||||
set: (val: Array<Order>) => {
|
||||
if (!props.public) {
|
||||
void store.updatePriceListColumnOrder(user.value, val);
|
||||
} else {
|
||||
_order.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const _columns: Array<Row> = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
field: 'name',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Kategorie',
|
||||
field: 'type',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
format: (val: FG.DrinkType) => val.name,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
field: 'tags',
|
||||
filterable: true,
|
||||
|
||||
format: (val: Array<FG.Tag>) => {
|
||||
let retVal = '';
|
||||
val.forEach((tag, index) => {
|
||||
if (index >= val.length - 1 && index > 0) {
|
||||
retVal += ', ';
|
||||
}
|
||||
retVal += tag.name;
|
||||
});
|
||||
return retVal;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'volume',
|
||||
label: 'Inhalt',
|
||||
field: 'volume',
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
format: (val: number) => `${val.toFixed(3)}L`,
|
||||
},
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Preis',
|
||||
field: 'price',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
format: (val: number) => `${val.toFixed(2)}€`,
|
||||
},
|
||||
{
|
||||
name: 'public',
|
||||
label: 'Öffentlich',
|
||||
field: 'public',
|
||||
format: (val: boolean) => (val ? 'Öffentlich' : 'nicht Öffentlich'),
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Beschreibung',
|
||||
field: 'description',
|
||||
filterable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const columns = computed(() => {
|
||||
const retVal: Array<Row> = [];
|
||||
if (order.value) {
|
||||
order.value.forEach((col) => {
|
||||
const _col = _columns.find((a) => a.name === col.name);
|
||||
if (_col) {
|
||||
retVal.push(_col);
|
||||
}
|
||||
});
|
||||
retVal.forEach((element, index) => {
|
||||
element.align = 'right';
|
||||
if (index === 0) {
|
||||
element.align = 'left';
|
||||
}
|
||||
});
|
||||
return retVal;
|
||||
}
|
||||
return _columns;
|
||||
});
|
||||
|
||||
const _options = computed(() => {
|
||||
const retVal: Array<{ name: string; label: string; field: string }> = [];
|
||||
columns.value.forEach((col) => {
|
||||
if (props.public) {
|
||||
if (col.name !== 'public') {
|
||||
retVal.push(col);
|
||||
}
|
||||
} else {
|
||||
retVal.push(col);
|
||||
}
|
||||
});
|
||||
return retVal;
|
||||
});
|
||||
|
||||
const _colums = computed<Array<string>>(() => {
|
||||
const retVal: Array<string> = [];
|
||||
columns.value.forEach((col) => {
|
||||
if (props.public) {
|
||||
if (col.name !== 'public') {
|
||||
retVal.push(col.name);
|
||||
}
|
||||
} else {
|
||||
retVal.push(col.name);
|
||||
}
|
||||
});
|
||||
return retVal;
|
||||
});
|
||||
|
||||
const _visibleColumns = ref(_colums.value);
|
||||
|
||||
const visibleColumns = computed({
|
||||
get: () => (props.public ? _visibleColumns.value : store.pricecalc_columns),
|
||||
set: (val) => {
|
||||
if (!props.public) {
|
||||
void store.updatePriceCalcColumn(user.value, val);
|
||||
} else {
|
||||
_visibleColumns.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const search = ref<Search>({
|
||||
value: '',
|
||||
key: '',
|
||||
label: '',
|
||||
});
|
||||
|
||||
const pagination = ref({
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 10,
|
||||
});
|
||||
|
||||
const fullscreen = ref(false);
|
||||
|
||||
return {
|
||||
drinks: computed(() => store.pricelist),
|
||||
columns,
|
||||
order,
|
||||
visibleColumns,
|
||||
options: _options,
|
||||
search,
|
||||
filter,
|
||||
pagination,
|
||||
fullscreen,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="sass">
|
||||
.ghost
|
||||
opacity: 0.5
|
||||
background: $accent
|
||||
</style>
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<q-dialog v-model="alert">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Suche</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<div>
|
||||
Wenn du in die Suche etwas eingibst, wird in allen Spalten gesucht. Mit einem `@` Zeichen,
|
||||
kann man die Suche eingrenzen auf eine Spalte. Zumbeispiel: `Tequilaparty@Tags`
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div>Mögliche Suchbegriffe nach dem @:</div>
|
||||
<div class="fit row q-gutter-sm">
|
||||
<div v-for="key in keys" :key="key.name">
|
||||
{{ key.label }}
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat label="OK" color="primary" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
<q-input v-model="v_model" filled dense>
|
||||
<template #append>
|
||||
<q-icon name="mdi-magnify" />
|
||||
</template>
|
||||
<template #prepend>
|
||||
<q-btn icon="mdi-help-circle" flat round @click="alert = true" />
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, PropType, ref } from 'vue';
|
||||
import { Search, Col } from '../utils/filter';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SearchInput',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as PropType<Search>,
|
||||
default: () => ({ value: '', key: undefined, label: '' }),
|
||||
},
|
||||
keys: {
|
||||
type: Object as PropType<Array<Col>>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:modelValue': (val: {
|
||||
value: string;
|
||||
key: string | undefined;
|
||||
label: string | undefined;
|
||||
}) => val,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const v_model = computed<string>({
|
||||
get: () => {
|
||||
if (!props.modelValue.label || props.modelValue.label === '') {
|
||||
return `${props.modelValue.value}`;
|
||||
}
|
||||
return `${props.modelValue.value}@${props.modelValue.label}`;
|
||||
},
|
||||
set: (val: string) => {
|
||||
const split = val.toLowerCase().split('@');
|
||||
if (split.length < 2) {
|
||||
emit('update:modelValue', { value: split[0], label: undefined, key: undefined });
|
||||
} else {
|
||||
props.keys.find((key) => {
|
||||
if (key.label.toLowerCase() === split[1]) {
|
||||
console.log(key.name);
|
||||
emit('update:modelValue', { value: split[0], label: split[1], key: key.name });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const alert = ref(false);
|
||||
return { v_model, alert };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
a
|
|
@ -0,0 +1,181 @@
|
|||
<template>
|
||||
<div>
|
||||
<q-page padding>
|
||||
<q-table
|
||||
title="Tags"
|
||||
:rows="rows"
|
||||
:row-key="(row) => row.id"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
>
|
||||
<template #top-right>
|
||||
<q-btn color="primary" icon="mdi-plus" round>
|
||||
<q-tooltip> Tag hinzufügen </q-tooltip>
|
||||
<q-menu v-model="popup" anchor="center middle" self="center middle" persistent>
|
||||
<q-input
|
||||
v-model="newTag.name"
|
||||
filled
|
||||
dense
|
||||
label="Name"
|
||||
class="q-pa-sm"
|
||||
:rule="[notExists]"
|
||||
/>
|
||||
<q-color
|
||||
:model-value="newTag.color"
|
||||
flat
|
||||
class="q-pa-sm"
|
||||
@change="
|
||||
(val) => {
|
||||
newTag.color = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="full-width row q-gutter-sm justify-around q-py-sm">
|
||||
<q-btn v-close-popup flat label="Abbrechen" />
|
||||
<q-btn flat label="Speichern" color="primary" @click="save" />
|
||||
</div>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</template>
|
||||
<template #header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width />
|
||||
</q-tr>
|
||||
</template>
|
||||
<template #body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="name" :props="props">
|
||||
{{ props.row.name }}
|
||||
<q-popup-edit
|
||||
v-model="props.row.name"
|
||||
buttons
|
||||
label-cancel="Abbrechen"
|
||||
label-set="Speichern"
|
||||
@update:modelValue="updateTag(props.row)"
|
||||
>
|
||||
<template #default="scope">
|
||||
<q-input v-model="scope.value" :rules="[notExists]" dense filled />
|
||||
</template>
|
||||
</q-popup-edit>
|
||||
</q-td>
|
||||
<q-td key="color" :props="props">
|
||||
<div class="full-width row q-gutter-sm justify-end items-center">
|
||||
<div>
|
||||
{{ props.row.color }}
|
||||
</div>
|
||||
<div class="color-box" :style="`background-color: ${props.row.color};`"> </div>
|
||||
</div>
|
||||
<q-popup-edit
|
||||
v-model="props.row.color"
|
||||
buttons
|
||||
label-cancel="Abbrechen"
|
||||
label-set="Speichern"
|
||||
@update:modelValue="updateTag(props.row)"
|
||||
>
|
||||
<template #default="slot">
|
||||
<div class="full-width row justify-center">
|
||||
<q-color
|
||||
:model-value="slot.value"
|
||||
class="full-width"
|
||||
flat
|
||||
@change="(val) => (slot.value = val)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</q-popup-edit>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
icon="mdi-delete"
|
||||
color="negative"
|
||||
round
|
||||
size="sm"
|
||||
@click="deleteTag(props.row)"
|
||||
/>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onBeforeMount, computed } from 'vue';
|
||||
import { usePricelistStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Tags',
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
onBeforeMount(() => {
|
||||
void store.getTags();
|
||||
});
|
||||
const columns = [
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
field: 'name',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
label: 'Farbe',
|
||||
field: 'color',
|
||||
},
|
||||
];
|
||||
const rows = computed(() => store.tags);
|
||||
const emptyTag = {
|
||||
id: -1,
|
||||
color: '#1976d2',
|
||||
name: '',
|
||||
};
|
||||
|
||||
async function save() {
|
||||
await store.setTag(newTag.value);
|
||||
popup.value = false;
|
||||
newTag.value = emptyTag;
|
||||
}
|
||||
|
||||
const newTag = ref(emptyTag);
|
||||
const popup = ref(false);
|
||||
function notExists(val: string) {
|
||||
const index = store.tags.findIndex((a) => a.name === val);
|
||||
if (index > -1) {
|
||||
return 'Tag existiert bereits.';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const pagination = ref({
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 10,
|
||||
});
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
newTag,
|
||||
popup,
|
||||
save,
|
||||
updateTag: store.updateTag,
|
||||
notExists,
|
||||
deleteTag: store.deleteTag,
|
||||
pagination,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-box {
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
max-width: 28px;
|
||||
max-height: 28px;
|
||||
border-width: 1px;
|
||||
border-color: black;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="q-table__title">Cocktailbuilder</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<ingredients
|
||||
v-model="volume.ingredients"
|
||||
class="q-pa-sm"
|
||||
editable
|
||||
@update:modelValue="update"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-table__title">Du solltest mindest sowiel verlangen oder bezahlen:</div>
|
||||
<div class="full-width row q-gutter-sm justify-around">
|
||||
<div v-for="min_price in volume.min_prices" :key="min_price.percentage">
|
||||
<div>
|
||||
<q-badge class="text-h6" color="primary"> {{ min_price.percentage }}% </q-badge>
|
||||
</div>
|
||||
<div>{{ min_price.price.toFixed(2) }}€</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeMount, ref } from 'vue';
|
||||
import Ingredients from '../components/CalculationTable/Ingredients.vue';
|
||||
import { DrinkPriceVolume, usePricelistStore } from '../store';
|
||||
import { calc_min_prices } from '../utils/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CocktailBuilder',
|
||||
components: { Ingredients },
|
||||
setup() {
|
||||
onBeforeMount(() => {
|
||||
void store.get_min_prices().finally(() => {
|
||||
volume.value.min_prices = calc_min_prices(volume.value, undefined, store.min_prices);
|
||||
});
|
||||
void store.getDrinks();
|
||||
void store.getDrinkTypes();
|
||||
void store.getExtraIngredients();
|
||||
});
|
||||
const store = usePricelistStore();
|
||||
|
||||
const emptyVolume: DrinkPriceVolume = {
|
||||
id: -1,
|
||||
_volume: 0,
|
||||
min_prices: [],
|
||||
prices: [],
|
||||
ingredients: [],
|
||||
};
|
||||
const volume = ref(emptyVolume);
|
||||
|
||||
function update() {
|
||||
volume.value.min_prices = calc_min_prices(volume.value, undefined, store.min_prices);
|
||||
}
|
||||
|
||||
return { volume, update };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<calculation-table v-if="!list" nodetails>
|
||||
<q-btn icon="mdi-view-list" round @click="list = !list">
|
||||
<q-tooltip> Zur Listenansicht wechseln </q-tooltip>
|
||||
</q-btn>
|
||||
</calculation-table>
|
||||
<pricelist v-if="list">
|
||||
<q-btn icon="mdi-cards-variant" round @click="list = !list">
|
||||
<q-tooltip> Zur Kartenansicht wechseln </q-tooltip>
|
||||
</q-btn>
|
||||
</pricelist>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeMount, computed } from 'vue';
|
||||
import CalculationTable from '../components/CalculationTable.vue';
|
||||
import Pricelist from '../components/Pricelist.vue';
|
||||
import { useMainStore } from '@flaschengeist/api';
|
||||
import { usePricelistStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InnerPricelist',
|
||||
components: { Pricelist, CalculationTable },
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
const mainStore = useMainStore();
|
||||
|
||||
onBeforeMount(() => {
|
||||
void store.getDrinks();
|
||||
void store.getPriceListView(mainStore.currentUser.userid);
|
||||
});
|
||||
|
||||
const list = computed({
|
||||
get: () => store.pricelist_view,
|
||||
set: (val: boolean) => store.updatePriceListView(mainStore.currentUser.userid, val),
|
||||
});
|
||||
return { list };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<calculation-table v-if="!list" public>
|
||||
<q-btn icon="mdi-view-list" round @click="list = !list">
|
||||
<q-tooltip> Zur Listenansicht wechseln </q-tooltip>
|
||||
</q-btn>
|
||||
</calculation-table>
|
||||
<pricelist v-if="list" public>
|
||||
<q-btn icon="mdi-cards-variant" round @click="list = !list">
|
||||
<q-tooltip> Zur Kartenansicht wechseln </q-tooltip>
|
||||
</q-btn>
|
||||
</pricelist>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import CalculationTable from '../components/CalculationTable.vue';
|
||||
import Pricelist from '../components/Pricelist.vue';
|
||||
import { usePricelistStore } from '../store';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OuterPricelist',
|
||||
components: { Pricelist, CalculationTable },
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
|
||||
onBeforeMount(() => {
|
||||
void store.getDrinks();
|
||||
});
|
||||
|
||||
const list = ref(false);
|
||||
return { list };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,176 @@
|
|||
<template>
|
||||
<q-table
|
||||
grid
|
||||
title="Rezepte"
|
||||
:rows="drinks"
|
||||
row-key="id"
|
||||
hide-header
|
||||
:filter="search"
|
||||
:filter-method="filter"
|
||||
:columns="options"
|
||||
>
|
||||
<template #top-right>
|
||||
<search-input v-model="search" :keys="search_keys" />
|
||||
</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"
|
||||
loading="lazy"
|
||||
:src="image(props.row.uuid)"
|
||||
placeholder-src="no-image.svg"
|
||||
>
|
||||
<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>
|
||||
<build-manual-volume :volumes="props.row.volumes" />
|
||||
<q-card-section>
|
||||
<div class="text-h6">Anleitung</div>
|
||||
<build-manual :steps="props.row.receipt" :editable="false" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
||||
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
|
||||
import BuildManualVolume from '../components/BuildManual/BuildManualVolume.vue';
|
||||
import SearchInput from '../components/SearchInput.vue';
|
||||
import { filter, Search } from '../utils/filter';
|
||||
import { usePricelistStore } from '../store';
|
||||
import { sort } from '../utils/sort';
|
||||
import { api } from '@flaschengeist/api';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Reciepts',
|
||||
components: { BuildManual, BuildManualVolume, SearchInput },
|
||||
setup() {
|
||||
const store = usePricelistStore();
|
||||
onBeforeMount(() => {
|
||||
void store.getDrinks();
|
||||
});
|
||||
const drinks = computed(() =>
|
||||
store.drinks.filter((drink) => {
|
||||
return drink.volumes.some((volume) => volume.ingredients.length > 0);
|
||||
})
|
||||
);
|
||||
|
||||
const columns_drinks = [
|
||||
{
|
||||
name: 'picture',
|
||||
label: 'Bild',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
field: 'name',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
filterable: 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,
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tag',
|
||||
field: 'tags',
|
||||
format: (val: Array<FG.Tag>) => {
|
||||
let retVal = '';
|
||||
val.forEach((tag, index) => {
|
||||
if (index > 0) {
|
||||
retVal += ', ';
|
||||
}
|
||||
retVal += tag.name;
|
||||
});
|
||||
return retVal;
|
||||
},
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
name: 'volumes',
|
||||
label: 'Preise',
|
||||
field: 'volumes',
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
const columns_volumes = [
|
||||
{
|
||||
name: 'volume',
|
||||
label: 'Inhalt',
|
||||
field: 'volume',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'prices',
|
||||
label: 'Preise',
|
||||
field: 'prices',
|
||||
},
|
||||
];
|
||||
const columns_prices = [
|
||||
{
|
||||
name: 'price',
|
||||
label: 'Preis',
|
||||
field: 'price',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Beschreibung',
|
||||
field: 'description',
|
||||
},
|
||||
{
|
||||
name: 'public',
|
||||
label: 'Öffentlich',
|
||||
field: 'public',
|
||||
},
|
||||
];
|
||||
|
||||
const search = ref<Search>({ value: '', key: '', label: '' });
|
||||
const search_keys = computed(() => columns_drinks.filter((column) => column.filterable));
|
||||
function image(uuid: string | undefined) {
|
||||
if (uuid) {
|
||||
return `${api.defaults.baseURL||''}/pricelist/picture/${uuid}?size=256`;
|
||||
}
|
||||
return 'no-image.svg';
|
||||
}
|
||||
return {
|
||||
drinks,
|
||||
options: [...columns_drinks, ...columns_volumes, ...columns_prices],
|
||||
search,
|
||||
filter,
|
||||
search_keys,
|
||||
image,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<div>
|
||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
||||
<q-tab
|
||||
v-for="(tabindex, index) in tabs"
|
||||
:key="'tab' + index"
|
||||
:name="tabindex.name"
|
||||
:label="tabindex.label"
|
||||
/>
|
||||
</q-tabs>
|
||||
<div v-else class="fit row justify-end">
|
||||
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer"></q-btn>
|
||||
</div>
|
||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
||||
<q-list v-model="tab">
|
||||
<q-item
|
||||
v-for="(tabindex, index) in tabs"
|
||||
:key="'tab' + index"
|
||||
:active="tab == tabindex.name"
|
||||
clickable
|
||||
@click="tab = tabindex.name"
|
||||
>
|
||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
<q-page paddding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
style="background-color: transparent"
|
||||
animated
|
||||
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
||||
>
|
||||
<q-tab-panel name="pricelist">
|
||||
<calculation-table editable />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="extra_ingredients">
|
||||
<extra-ingredients />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="drink_types">
|
||||
<drink-types />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="tags">
|
||||
<tags />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import CalculationTable from '../components/CalculationTable.vue';
|
||||
import ExtraIngredients from '../components/ExtraIngredients.vue';
|
||||
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
|
||||
import DrinkTypes from '../components/DrinkTypes.vue';
|
||||
import { hasPermissions } from '@flaschengeist/api';
|
||||
import { usePricelistStore } from '../store';
|
||||
import Tags from '../components/Tags.vue';
|
||||
import { Screen } from 'quasar';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Settings',
|
||||
components: { ExtraIngredients, DrinkTypes, Tags, CalculationTable },
|
||||
setup() {
|
||||
interface Tab {
|
||||
name: string;
|
||||
label: string;
|
||||
permissions: Array<string>;
|
||||
}
|
||||
const store = usePricelistStore();
|
||||
onBeforeMount(() => {
|
||||
store
|
||||
.getExtraIngredients()
|
||||
.then(() => {
|
||||
console.log(store.extraIngredients);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
void store.getTags();
|
||||
void store.getDrinkTypes();
|
||||
void store.getDrinks();
|
||||
void store.get_min_prices();
|
||||
});
|
||||
|
||||
const drawer = ref<boolean>(false);
|
||||
|
||||
const showDrawer = computed({
|
||||
get: () => {
|
||||
return !Screen.gt.sm && drawer.value;
|
||||
},
|
||||
set: (val: boolean) => {
|
||||
drawer.value = val;
|
||||
},
|
||||
});
|
||||
|
||||
const _tabs: Tab[] = [
|
||||
{ name: 'pricelist', label: 'Getränke', permissions: ['drink_edit'] },
|
||||
{
|
||||
name: 'extra_ingredients',
|
||||
label: 'Zutaten',
|
||||
permissions: ['edit_ingredients', 'delete_ingredients'],
|
||||
},
|
||||
{
|
||||
name: 'drink_types',
|
||||
label: 'Getränketypen',
|
||||
permissions: ['drink_type_edit', 'drink_type_delete'],
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
permissions: ['drink_tag_edit', 'drink_tag_create', 'drink_tag_delete'],
|
||||
},
|
||||
];
|
||||
|
||||
const tabs = computed(() => {
|
||||
const retVal: Tab[] = [];
|
||||
_tabs.forEach((tab) => {
|
||||
if (tab.permissions.length > 0) {
|
||||
if (hasPermissions(tab.permissions)) {
|
||||
retVal.push(tab);
|
||||
}
|
||||
}
|
||||
if (tab.permissions.length === 0) {
|
||||
retVal.push(tab);
|
||||
}
|
||||
});
|
||||
return retVal;
|
||||
});
|
||||
|
||||
const tab = ref<string>('pricelist');
|
||||
|
||||
return { tabs, tab, showDrawer };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,33 @@
|
|||
export const PERMISSIONS = {
|
||||
CREATE: 'drink_create',
|
||||
|
||||
EDIT: 'drink_edit',
|
||||
|
||||
DELETE: 'drink_delete',
|
||||
|
||||
CREATE_TAG: 'drink_tag_create',
|
||||
|
||||
EDIT_PRICE: 'edit_price',
|
||||
DELETE_PRICE: 'delete_price',
|
||||
|
||||
EDIT_VOLUME: 'edit_volume',
|
||||
DELETE_VOLUME: 'delete_volume',
|
||||
|
||||
EDIT_INGREDIENTS_DRINK: 'edit_ingredients_drink',
|
||||
DELETE_INGREDIENTS_DRINK: 'delete_ingredients_drink',
|
||||
|
||||
EDIT_INGREDIENTS: 'edit_ingredients',
|
||||
DELETE_INGREDIENTS: 'delete_ingredients',
|
||||
|
||||
EDIT_TAG: 'drink_tag_edit',
|
||||
|
||||
DELETE_TAG: 'drink_tag_delete',
|
||||
|
||||
CREATE_TYPE: 'drink_type_create',
|
||||
|
||||
EDIT_TYPE: 'drink_type_edit',
|
||||
|
||||
DELETE_TYPE: 'drink_type_delete',
|
||||
|
||||
EDIT_MIN_PRICES: 'edit_min_prices',
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import { innerRoutes, outerRoutes } from './routes';
|
||||
import { FG_Plugin } from '@flaschengeist/typings';
|
||||
|
||||
const plugin: FG_Plugin.Plugin = {
|
||||
id: 'pricelist',
|
||||
name: 'Pricelist',
|
||||
innerRoutes,
|
||||
outerRoutes,
|
||||
requiredModules: [],
|
||||
requiredBackendModules: ['pricelist'],
|
||||
version: '0.0.1',
|
||||
widgets: [],
|
||||
};
|
||||
|
||||
export default plugin;
|
|
@ -0,0 +1,73 @@
|
|||
import { FG_Plugin } from '@flaschengeist/types';
|
||||
|
||||
export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||
{
|
||||
title: 'Getränke',
|
||||
icon: 'mdi-glass-mug-variant',
|
||||
route: {
|
||||
path: 'drinks',
|
||||
name: 'drinks',
|
||||
redirect: { name: 'drinks-pricelist' },
|
||||
},
|
||||
permissions: ['user'],
|
||||
children: [
|
||||
{
|
||||
title: 'Preisliste',
|
||||
icon: 'mdi-cash-100',
|
||||
shortcut: true,
|
||||
permissions: ['user'],
|
||||
route: {
|
||||
path: 'pricelist',
|
||||
name: 'drinks-pricelist',
|
||||
component: () => import('../pages/InnerPricelist.vue'),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Rezepte',
|
||||
shortcut: false,
|
||||
icon: 'mdi-receipt',
|
||||
permissions: ['user'],
|
||||
route: {
|
||||
path: 'reciepts',
|
||||
name: 'reciepts',
|
||||
component: () => import('../pages/Receipts.vue'),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Cocktailbuilder',
|
||||
shortcut: false,
|
||||
icon: 'mdi-glass-cocktail',
|
||||
permissions: ['user'],
|
||||
route: {
|
||||
path: 'cocktail-builder',
|
||||
name: 'cocktail-builder',
|
||||
component: () => import('../pages/CocktailBuilder.vue'),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Einstellungen',
|
||||
icon: 'mdi-coffee-to-go',
|
||||
shortcut: false,
|
||||
permissions: ['drink_edit', 'drink_tag_edit'],
|
||||
route: {
|
||||
path: 'settings',
|
||||
name: 'drinks-settings',
|
||||
component: () => import('../pages/Settings.vue'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const outerRoutes: FG_Plugin.MenuRoute[] = [
|
||||
{
|
||||
title: 'Preisliste',
|
||||
icon: 'mdi-glass-mug-variant',
|
||||
shortcut: true,
|
||||
route: {
|
||||
path: 'pricelist',
|
||||
name: 'outter-pricelist',
|
||||
component: () => import('../pages/OuterPricelist.vue'),
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,5 @@
|
|||
declare module '*.vue' {
|
||||
import { ComponentOptions } from 'vue'
|
||||
const component: ComponentOptions
|
||||
export default component
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
import { api } from '@flaschengeist/api';
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
calc_volume,
|
||||
calc_cost_per_volume,
|
||||
calc_all_min_prices,
|
||||
} from './utils/utils';
|
||||
|
||||
interface DrinkPriceVolume extends Omit<FG.DrinkPriceVolume, 'volume'> {
|
||||
_volume: number;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
interface Drink extends Omit<Omit<FG.Drink, 'cost_per_volume'>, 'volumes'> {
|
||||
volumes: DrinkPriceVolume[];
|
||||
cost_per_volume?: number;
|
||||
_cost_per_volume?: number;
|
||||
}
|
||||
|
||||
interface Pricelist {
|
||||
name: string;
|
||||
type: FG.DrinkType;
|
||||
tags: Array<FG.Tag>;
|
||||
volume: number;
|
||||
price: number;
|
||||
public: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
class DrinkPriceVolume implements DrinkPriceVolume {
|
||||
constructor({ id, volume, prices, ingredients }: FG.DrinkPriceVolume) {
|
||||
this.id = id;
|
||||
this._volume = volume;
|
||||
this.prices = prices;
|
||||
this.ingredients = ingredients;
|
||||
this.min_prices = [];
|
||||
this.volume = calc_volume(this);
|
||||
}
|
||||
}
|
||||
|
||||
class Drink {
|
||||
constructor({
|
||||
id,
|
||||
article_id,
|
||||
package_size,
|
||||
name,
|
||||
volume,
|
||||
cost_per_volume,
|
||||
cost_per_package,
|
||||
tags,
|
||||
type,
|
||||
uuid,
|
||||
receipt,
|
||||
}: FG.Drink) {
|
||||
this.id = id;
|
||||
this.article_id = article_id;
|
||||
this.package_size = package_size;
|
||||
this.name = name;
|
||||
this.volume = volume;
|
||||
this.cost_per_package = cost_per_package;
|
||||
this._cost_per_volume = cost_per_volume;
|
||||
this.cost_per_volume = calc_cost_per_volume(this);
|
||||
this.tags = tags;
|
||||
this.type = type;
|
||||
this.volumes = [];
|
||||
this.uuid = uuid;
|
||||
this.receipt = receipt || [];
|
||||
}
|
||||
}
|
||||
|
||||
interface Order {
|
||||
label: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const usePricelistStore = defineStore({
|
||||
id: 'pricelist',
|
||||
|
||||
state: () => ({
|
||||
drinkTypes: [] as Array<FG.DrinkType>,
|
||||
drinks: [] as Array<Drink>,
|
||||
extraIngredients: [] as Array<FG.ExtraIngredient>,
|
||||
min_prices: [] as Array<number>,
|
||||
tags: [] as Array<FG.Tag>,
|
||||
pricecalc_columns: [] as Array<string>,
|
||||
pricelist_view: false as boolean,
|
||||
pricelist_columns_order: [] as Array<Order>,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async getDrinkTypes(force = false) {
|
||||
if (force || this.drinks.length == 0) {
|
||||
const { data } = await api.get<Array<FG.DrinkType>>('/pricelist/drink-types');
|
||||
this.drinkTypes = data;
|
||||
}
|
||||
return this.drinkTypes;
|
||||
},
|
||||
async addDrinkType(name: string) {
|
||||
const { data } = await api.post<FG.DrinkType>('/pricelist/drink-types', { name: name });
|
||||
this.drinkTypes.push(data);
|
||||
},
|
||||
async removeDrinkType(id: number) {
|
||||
await api.delete(`/pricelist/drink-types/${id}`);
|
||||
const idx = this.drinkTypes.findIndex((val) => val.id == id);
|
||||
if (idx >= 0) this.drinkTypes.splice(idx, 1);
|
||||
},
|
||||
async changeDrinkTypeName(drinkType: FG.DrinkType) {
|
||||
await api.put(`/pricelist/drink-types/${drinkType.id}`, drinkType);
|
||||
const itm = this.drinkTypes.filter((val) => val.id == drinkType.id);
|
||||
if (itm.length > 0) itm[0].name = drinkType.name;
|
||||
},
|
||||
async getExtraIngredients() {
|
||||
const { data } = await api.get<Array<FG.ExtraIngredient>>(
|
||||
'pricelist/ingredients/extraIngredients'
|
||||
);
|
||||
this.extraIngredients = data;
|
||||
},
|
||||
async setExtraIngredient(ingredient: FG.ExtraIngredient) {
|
||||
const { data } = await api.post<FG.ExtraIngredient>(
|
||||
'pricelist/ingredients/extraIngredients',
|
||||
ingredient
|
||||
);
|
||||
this.extraIngredients.push(data);
|
||||
},
|
||||
async updateExtraIngredient(ingredient: FG.ExtraIngredient) {
|
||||
const { data } = await api.put<FG.ExtraIngredient>(
|
||||
`pricelist/ingredients/extraIngredients/${ingredient.id}`,
|
||||
ingredient
|
||||
);
|
||||
const index = this.extraIngredients.findIndex((a) => a.id === ingredient.id);
|
||||
if (index > -1) {
|
||||
this.extraIngredients[index] = data;
|
||||
} else {
|
||||
this.extraIngredients.push(data);
|
||||
}
|
||||
},
|
||||
async deleteExtraIngredient(ingredient: FG.ExtraIngredient) {
|
||||
await api.delete(`pricelist/ingredients/extraIngredients/${ingredient.id}`);
|
||||
const index = this.extraIngredients.findIndex((a) => a.id === ingredient.id);
|
||||
if (index > -1) {
|
||||
this.extraIngredients.splice(index, 1);
|
||||
}
|
||||
},
|
||||
async getDrinks() {
|
||||
const { data } = await api.get<Array<FG.Drink>>('pricelist/drinks');
|
||||
this.drinks = [];
|
||||
data.forEach((drink) => {
|
||||
const _drink = new Drink(drink);
|
||||
drink.volumes.forEach((volume) => {
|
||||
const _volume = new DrinkPriceVolume(volume);
|
||||
_drink.volumes.push(_volume);
|
||||
});
|
||||
this.drinks.push(_drink);
|
||||
});
|
||||
calc_all_min_prices(this.drinks, this.min_prices);
|
||||
},
|
||||
sortPrices(volume: DrinkPriceVolume) {
|
||||
volume.prices.sort((a, b) => {
|
||||
if (a.price > b.price) return 1;
|
||||
if (b.price > a.price) return -1;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
async deletePrice(price: FG.DrinkPrice) {
|
||||
await api.delete(`pricelist/prices/${price.id}`);
|
||||
},
|
||||
async deleteVolume(volume: DrinkPriceVolume, drink: Drink) {
|
||||
await api.delete(`pricelist/volumes/${volume.id}`);
|
||||
const index = drink.volumes.findIndex((a) => a.id === volume.id);
|
||||
if (index > -1) {
|
||||
drink.volumes.splice(index, 1);
|
||||
}
|
||||
},
|
||||
async deleteIngredient(ingredient: FG.Ingredient) {
|
||||
await api.delete(`pricelist/ingredients/${ingredient.id}`);
|
||||
},
|
||||
async setDrink(drink: Drink) {
|
||||
const { data } = await api.post<FG.Drink>('pricelist/drinks', {
|
||||
...drink,
|
||||
});
|
||||
const _drink = new Drink(data);
|
||||
data.volumes.forEach((volume) => {
|
||||
const _volume = new DrinkPriceVolume(volume);
|
||||
_drink.volumes.push(_volume);
|
||||
});
|
||||
this.drinks.push(_drink);
|
||||
calc_all_min_prices(this.drinks, this.min_prices);
|
||||
return _drink;
|
||||
},
|
||||
async updateDrink(drink: Drink) {
|
||||
const { data } = await api.put<FG.Drink>(`pricelist/drinks/${drink.id}`, {
|
||||
...drink,
|
||||
});
|
||||
const index = this.drinks.findIndex((a) => a.id === data.id);
|
||||
if (index > -1) {
|
||||
const _drink = new Drink(data);
|
||||
data.volumes.forEach((volume) => {
|
||||
const _volume = new DrinkPriceVolume(volume);
|
||||
_drink.volumes.push(_volume);
|
||||
});
|
||||
this.drinks[index] = _drink;
|
||||
}
|
||||
calc_all_min_prices(this.drinks, this.min_prices);
|
||||
},
|
||||
deleteDrink(drink: Drink) {
|
||||
api
|
||||
.delete(`pricelist/drinks/${drink.id}`)
|
||||
.then(() => {
|
||||
const index = this.drinks.findIndex((a) => a.id === drink.id);
|
||||
if (index > -1) {
|
||||
this.drinks.splice(index, 1);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.warn(err));
|
||||
},
|
||||
async get_min_prices() {
|
||||
const { data } = await api.get<Array<number>>('pricelist/settings/min_prices');
|
||||
this.min_prices = data;
|
||||
},
|
||||
async set_min_prices() {
|
||||
await api.post<Array<number>>('pricelist/settings/min_prices', this.min_prices);
|
||||
calc_all_min_prices(this.drinks, this.min_prices);
|
||||
},
|
||||
async upload_drink_picture(drink: Drink, file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const { data } = await api.post<FG.Drink>(`pricelist/drinks/${drink.id}/picture`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
const _drink = this.drinks.find((a) => a.id === drink.id);
|
||||
if (_drink) {
|
||||
_drink.uuid = data.uuid;
|
||||
}
|
||||
},
|
||||
async delete_drink_picture(drink: Drink) {
|
||||
await api.delete(`pricelist/drinks/${drink.id}/picture`);
|
||||
drink.uuid = '';
|
||||
},
|
||||
async getTags() {
|
||||
const { data } = await api.get<Array<FG.Tag>>('/pricelist/tags');
|
||||
this.tags = data;
|
||||
},
|
||||
async setTag(tag: FG.Tag) {
|
||||
const { data } = await api.post<FG.Tag>('/pricelist/tags', tag);
|
||||
this.tags.push(data);
|
||||
},
|
||||
async updateTag(tag: FG.Tag) {
|
||||
const { data } = await api.put<FG.Tag>(`/pricelist/tags/${tag.id}`, tag);
|
||||
const index = this.tags.findIndex((a) => a.id === data.id);
|
||||
if (index > -1) {
|
||||
this.tags[index] = data;
|
||||
}
|
||||
},
|
||||
async deleteTag(tag: FG.Tag) {
|
||||
await api.delete(`/pricelist/tags/${tag.id}`);
|
||||
const index = this.tags.findIndex((a) => a.id === tag.id);
|
||||
if (index > -1) {
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
},
|
||||
async getPriceCalcColumn(userid: string) {
|
||||
const { data } = await api.get<Array<string>>(`pricelist/users/${userid}/pricecalc_columns`);
|
||||
this.pricecalc_columns = data;
|
||||
},
|
||||
async updatePriceCalcColumn(userid: string, data: Array<string>) {
|
||||
await api.put<Array<string>>(`pricelist/users/${userid}/pricecalc_columns`, data);
|
||||
this.pricecalc_columns = data;
|
||||
},
|
||||
async getPriceListView(userid: string) {
|
||||
const { data } = await api.get<{ value: boolean }>(`pricelist/users/${userid}/pricelist`);
|
||||
this.pricelist_view = data.value;
|
||||
},
|
||||
async updatePriceListView(userid: string, data: boolean) {
|
||||
await api.put<Array<string>>(`pricelist/users/${userid}/pricelist`, { value: data });
|
||||
this.pricelist_view = data;
|
||||
},
|
||||
async getPriceListColumnOrder(userid: string) {
|
||||
const { data } = await api.get<Array<Order>>(
|
||||
`pricelist/users/${userid}/pricecalc_columns_order`
|
||||
);
|
||||
this.pricelist_columns_order = data;
|
||||
},
|
||||
async updatePriceListColumnOrder(userid: string, data: Array<Order>) {
|
||||
await api.put<Array<string>>(`pricelist/users/${userid}/pricecalc_columns_order`, data);
|
||||
this.pricelist_columns_order = data;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
pricelist() {
|
||||
const retVal: Array<Pricelist> = [];
|
||||
this.drinks.forEach((drink) => {
|
||||
drink.volumes.forEach((volume) => {
|
||||
volume.prices.forEach((price) => {
|
||||
retVal.push({
|
||||
name: drink.name,
|
||||
type: <FG.DrinkType>drink.type,
|
||||
tags: <Array<FG.Tag>>drink.tags,
|
||||
volume: <number>volume.volume,
|
||||
price: price.price,
|
||||
public: price.public,
|
||||
description: <string>price.description,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return retVal;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { DrinkPriceVolume, Drink, Order };
|
|
@ -0,0 +1,39 @@
|
|||
import { Drink } from '../store';
|
||||
|
||||
function filter(
|
||||
rows: Array<Drink>,
|
||||
terms: Search,
|
||||
cols: Array<Col>,
|
||||
cellValue: { (col: Col, row: Drink): string }
|
||||
) {
|
||||
if (terms.value) {
|
||||
return rows.filter((row) => {
|
||||
if (!terms.key || terms.key === '') {
|
||||
return cols.some((col) => {
|
||||
const val = cellValue(col, row) + '';
|
||||
const haystack = val === 'undefined' || val === 'null' ? '' : val.toLowerCase();
|
||||
return haystack.indexOf(terms.value) !== -1;
|
||||
});
|
||||
}
|
||||
const index = cols.findIndex((col) => col.name === terms.key);
|
||||
const val = cellValue(cols[index], row) + '';
|
||||
const haystack = val === 'undefined' || val === 'null' ? '' : val.toLowerCase();
|
||||
return haystack.indexOf(terms.value) !== -1;
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
interface Search {
|
||||
value: string;
|
||||
label: string | undefined;
|
||||
key: string | undefined;
|
||||
}
|
||||
|
||||
interface Col {
|
||||
name: string;
|
||||
label: string;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export { filter, Search, Col };
|
|
@ -0,0 +1,7 @@
|
|||
function sort(a: string | number, b: string | number) {
|
||||
if (a > b) return 1;
|
||||
if (b > a) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export { sort };
|
|
@ -0,0 +1,84 @@
|
|||
import { Drink, DrinkPriceVolume, usePricelistStore } from '../store';
|
||||
|
||||
function calc_volume(volume: DrinkPriceVolume) {
|
||||
if (volume.ingredients.some((ingredient) => !!ingredient.drink_ingredient)) {
|
||||
let retVal = 0;
|
||||
volume.ingredients.forEach((ingredient) => {
|
||||
if (ingredient.drink_ingredient?.volume) {
|
||||
retVal += ingredient.drink_ingredient.volume;
|
||||
}
|
||||
});
|
||||
return retVal;
|
||||
} else {
|
||||
return volume._volume;
|
||||
}
|
||||
}
|
||||
|
||||
function calc_cost_per_volume(drink: Drink) {
|
||||
let retVal = drink._cost_per_volume;
|
||||
if (!!drink.volume && !!drink.package_size && !!drink.cost_per_package) {
|
||||
retVal =
|
||||
((drink.cost_per_package || 0) / ((drink.volume || 0) * (drink.package_size || 0))) * 1.19;
|
||||
}
|
||||
|
||||
return retVal ? Math.round(retVal * 1000) / 1000 : retVal;
|
||||
}
|
||||
|
||||
function calc_all_min_prices(drinks: Array<Drink>, min_prices: Array<number>) {
|
||||
drinks.forEach((drink) => {
|
||||
drink.volumes.forEach((volume) => {
|
||||
volume.min_prices = calc_min_prices(volume, drink.cost_per_volume, min_prices);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function helper(volume: DrinkPriceVolume, min_price: number) {
|
||||
let retVal = 0;
|
||||
let extraIngredientPrice = 0;
|
||||
volume.ingredients.forEach((ingredient) => {
|
||||
if (ingredient.drink_ingredient) {
|
||||
const _drink = usePricelistStore().drinks.find(
|
||||
(a) => a.id === ingredient.drink_ingredient?.ingredient_id
|
||||
);
|
||||
retVal += ingredient.drink_ingredient.volume * <number>(<unknown>_drink?.cost_per_volume);
|
||||
}
|
||||
if (ingredient.extra_ingredient) {
|
||||
extraIngredientPrice += ingredient.extra_ingredient.price;
|
||||
}
|
||||
});
|
||||
return (retVal * min_price) / 100 + extraIngredientPrice;
|
||||
}
|
||||
|
||||
function calc_min_prices(
|
||||
volume: DrinkPriceVolume,
|
||||
cost_per_volume: number | undefined,
|
||||
min_prices: Array<number>
|
||||
) {
|
||||
const retVal: Array<FG.MinPrices> = [];
|
||||
volume.min_prices = [];
|
||||
if (min_prices) {
|
||||
min_prices.forEach((min_price) => {
|
||||
let computedMinPrice: number;
|
||||
if (cost_per_volume) {
|
||||
computedMinPrice = (cost_per_volume * <number>volume.volume * min_price) / 100;
|
||||
} else {
|
||||
computedMinPrice = helper(volume, min_price);
|
||||
}
|
||||
retVal.push({ percentage: min_price, price: computedMinPrice });
|
||||
});
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
function clone<T>(o: T): T {
|
||||
return <T>JSON.parse(JSON.stringify(o));
|
||||
}
|
||||
|
||||
interface DeleteObjects {
|
||||
prices: Array<FG.DrinkPrice>;
|
||||
volumes: Array<DrinkPriceVolume>;
|
||||
ingredients: Array<FG.Ingredient>;
|
||||
}
|
||||
export { DeleteObjects };
|
||||
|
||||
export { calc_volume, calc_cost_per_volume, calc_all_min_prices, calc_min_prices, clone };
|
Loading…
Reference in New Issue