release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
8 changed files with 323 additions and 148 deletions
Showing only changes of commit 7a3a151688 - Show all commits

View File

@ -371,8 +371,13 @@
props.row.uuid ? `/api/pricelist/picture/${props.row.uuid}?size=256` : 'no-image.svg'
"
>
<div class='absolute-top-right justify-end' style='background-color: transparent;'>
<q-btn @click="console.log('hallo')" round icon='mdi-pencil' style='background-color: rgba(0,0,0,0.5)'/>
<div 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">
@ -447,14 +452,20 @@
/>
</div>
</q-card-section>
<q-card-section v-if="props.row.volumes.length > 0">
<drink-price-volumes :props="props.row" />
<drink-price-volumes v-model="props.row.volumes" />
</q-card-section>
</q-card>
</div>
</template>
</q-table>
<q-dialog :model-value="editDrink !== undefined" persistent>
<drink-modify
:drink="editDrink"
@save="editDrink = undefined"
@cancel="editDrink = undefined"
/>
</q-dialog>
</template>
<script lang="ts">
@ -467,6 +478,7 @@ import NewDrink from 'src/plugins/pricelist/components/CalculationTable/NewDrink
import BuildManual from 'src/plugins/pricelist/components/CalculationTable/BuildManual.vue';
import SearchInput from './SearchInput.vue';
import DrinkPriceVolumes from 'src/plugins/pricelist/components/CalculationTable/DrinkPriceVolumes.vue';
import DrinkModify from './DrinkModify.vue';
import { filter, Search } from '../utils/filter';
import { Notify } from 'quasar';
import { sort } from '../utils/sort';
@ -480,6 +492,7 @@ export default defineComponent({
BuildManual,
DrinkPriceVolumesTable,
DrinkPriceVolumes,
DrinkModify,
},
setup() {
const mainStore = useMainStore();
@ -715,6 +728,7 @@ export default defineComponent({
label: '',
});
const grid = ref(true);
const editDrink = ref();
return {
drinks: computed(() => store.drinks),
pagination,
@ -737,6 +751,7 @@ export default defineComponent({
search_keys,
grid,
tags: computed(() => store.tags),
editDrink,
};
},
});

View File

@ -37,7 +37,7 @@ export default defineComponent({
const emptyVolume: DrinkPriceVolume = {
id: -1,
_volume: 0,
min_prices: [{ percentage: 100 }, { percentage: 250 }, { percentage: 300 }],
min_prices: [],
prices: [],
ingredients: [],
};

View File

@ -9,17 +9,20 @@
navigation
arrows
>
<q-carousel-slide v-for="volume in props.volumes" :key="volume.id" :name="volume.id">
<q-carousel-slide v-for="volume in volumes" :key="volume.id" :name="volume.id">
<q-input
v-model.number="volume.volume"
class="q-pa-sm"
:model-value="volume.volume"
outlined
readonly
:outlined="!editable"
:filled="editable"
:readonly="!editable"
dense
label="Inhalt"
mask="#.###"
fill-mask="0"
suffix="L"
min="0"
step="0.001"
/>
<div class="full-width row q-gutter-sm q-pa-sm justify-around">
<div v-for="(min_price, index) in volume.min_prices" :key="index">
@ -30,58 +33,104 @@
<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 class="text-body1 col-xs-12 col-md-4">{{ price.price.toFixed(2) }}</div>
<div class="text-body1 col-xs-12 col-md-4">
<div v-if="!editable" class="text-body1 col-xs-12 col-md-3">
{{ price.price.toFixed(2) }}
</div>
<q-input
v-else
v-model.number="price.price"
class="col-xs-12 col-md-3"
type="number"
min="0"
step="0.01"
suffix="€"
filled
dense
label="Preis"
/>
<div class="text-body1 col-xs-12 col-md-2">
<q-toggle
:model-value="price.public"
disable
v-model="price.public"
:disable="!editable"
checked-icon="mdi-earth"
unchecked-icon="mdi-earth-off"
/>
</div>
<div class="text-body1 col-xs-12 col-md-4">
<div v-if="!editable" class="text-body1 col-xs-12 col-md-5">
{{ price.description }}
</div>
<q-input
v-else
v-model="price.description"
class="col-xs-12 col-md-5"
filled
dense
label="Beschreibung"
/>
</div>
<q-separator v-if="index < volume.prices.length - 1" />
</div>
</div>
<div class="q-pa-sm">
<ingredients :ingredients="volume.ingredients" :volume="volume" />
<ingredients v-model="volume.ingredients" :editable="editable" />
</div>
</q-carousel-slide>
</q-carousel>
<q-btn-toggle v-model="volume" :options="options" />
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue';
import { Drink } from 'src/plugins/pricelist/store';
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
import { DrinkPriceVolume } from 'src/plugins/pricelist/store';
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
export default defineComponent({
name: 'DrinkPriceVolume',
components: { Ingredients },
props: {
props: {
type: Object as PropType<Drink>,
modelValue: {
type: Array as PropType<Array<DrinkPriceVolume>>,
required: true,
},
editable: {
type: Boolean,
default: false,
},
setup(props) {
const _volume = ref<number|undefined>();
const volume = computed<number|undefined>({
},
emits: {
'update:modelValue': (val: Array<DrinkPriceVolume>) => val,
},
setup(props, { emit }) {
onBeforeMount(() => {
volumes.value = <Array<DrinkPriceVolume>>[...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 (props.props.volumes.length > 0) {
return props.props.volumes[0].id;
if (volumes.value.length > 0) {
return volumes.value[0].id;
}
return undefined;
},
set: (val: number|undefined) => (_volume.value = val),
set: (val: number | undefined) => (_volume.value = val),
});
return { volume };
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}L`, value: volume.id });
});
return retVal;
});
function updateVolume() {
emit('update:modelValue', volumes.value);
}
return { volumes, volume, options, updateVolume };
},
});
</script>

View File

@ -1,8 +1,8 @@
<template>
<div class="full-width">
<div
v-for="ingredient in ingredients"
:key="`volume:${volume.id},ingredient:${ingredient.id}`"
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">
@ -11,14 +11,16 @@
<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="updateDrink"
@save="updateValue"
>
<q-select
v-model="ingredient.drink_ingredient.ingredient_id"
v-model="scope.ingredient_id"
class="col q-px-sm"
label="Getränk"
filled
@ -34,14 +36,16 @@
<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="updateDrink"
@save="updateValue"
>
<q-input
v-model.number="ingredient.drink_ingredient.volume"
v-model.number="scope.value"
class="col q-px-sm"
label="Volume"
type="number"
@ -63,11 +67,12 @@
<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="updateDrink"
@save="updateValue"
>
<q-select
v-model="ingredient.extra_ingredient"
@ -78,19 +83,19 @@
/>
</q-popup-edit>
</div>
<div class="col-1 row justify-end q-pa-xs">
<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, volume)"
@click="deleteIngredient(ingredient)"
/>
</div>
</div>
<q-separator />
</div>
<div class="full-width row justify-end q-py-xs">
<div v-if="editable" class="full-width row justify-end q-py-xs">
<q-btn size="sm" icon-right="mdi-plus" color="positive" label="Zutat hinzufügen">
<q-menu anchor="center middle" self="center middle">
<div class="full-width row justify-around q-gutter-sm q-pa-sm">
@ -133,12 +138,7 @@
</div>
<div class="full-width row jusitfy-between q-gutter-sm q-pa-sm">
<q-btn v-close-popup label="Abbrechen" @click="cancelAddIngredient" />
<q-btn
v-close-popup
label="Speichern"
color="positive"
@click="addIngredient(volume)"
/>
<q-btn v-close-popup label="Speichern" color="positive" @click="addIngredient" />
</div>
</q-menu>
</q-btn>
@ -147,34 +147,39 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue';
import { DrinkPriceVolume, usePricelistStore } from '../../store';
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
import { usePricelistStore } from '../../store';
export default defineComponent({
name: 'Ingredients',
props: {
ingredients: {
type: Array as PropType<FG.Ingredient[]>,
modelValue: {
type: Object as PropType<Array<FG.Ingredient>>,
required: true,
},
volume: {
type: Object /*as PropType<DrinkPriceVolume>*/,
required: true,
editable: {
type: Boolean,
default: false,
},
},
emits: {
updateDrink: () => true,
addIngredient: (val: FG.Ingredient, volume: DrinkPriceVolume) =>
(val.drink_ingredient || val.extra_ingredient) && volume,
deleteIngredient: (ingredient: FG.Ingredient, volume: DrinkPriceVolume) =>
(ingredient.drink_ingredient || ingredient.extra_ingredient) && volume,
'update:modelValue': (val: Array<FG.Ingredient>) => val,
},
setup(_, { emit }) {
setup(props, { emit }) {
onBeforeMount(() => {
edit_ingredients.value = [];
props.modelValue.forEach((a) => {
edit_ingredients.value.push(Object.assign({}, a));
});
console.log(edit_ingredients);
});
const store = usePricelistStore();
const edit_ingredients = ref<Array<FG.Ingredient>>([]);
const newIngredient = ref<FG.Drink | FG.ExtraIngredient>();
const newIngredientVolume = ref<number>(0);
function addIngredient(volume: DrinkPriceVolume) {
function addIngredient() {
let _ingredient: FG.Ingredient;
if ((<FG.Drink>newIngredient.value)?.volume && newIngredient.value) {
_ingredient = {
@ -193,20 +198,27 @@ export default defineComponent({
extra_ingredient: <FG.ExtraIngredient>newIngredient.value,
};
}
emit('addIngredient', _ingredient, volume);
edit_ingredients.value.push(_ingredient);
emit('update:modelValue', edit_ingredients.value);
cancelAddIngredient();
}
function updateDrink() {
console.log('updateDrink from Ingredients');
emit('updateDrink');
function updateValue(value: number, initValue: number) {
console.log('updateValue', value, initValue);
emit('update:modelValue', edit_ingredients.value);
}
function cancelAddIngredient() {
setTimeout(() => {
(newIngredient.value = undefined), (newIngredientVolume.value = 0);
}, 200);
}
function deleteIngredient(ingredient: FG.Ingredient, volume: DrinkPriceVolume) {
emit('deleteIngredient', ingredient, volume);
function deleteIngredient(ingredient: FG.Ingredient) {
const index = edit_ingredients.value.findIndex((a) => a.id === ingredient.id);
if (index > -1) {
edit_ingredients.value.splice(index, 1);
}
emit('update:modelValue', edit_ingredients.value);
}
const drinks = computed(() =>
store.drinks.filter((drink) => {
@ -227,9 +239,10 @@ export default defineComponent({
newIngredient,
newIngredientVolume,
cancelAddIngredient,
updateDrink,
updateValue,
deleteIngredient,
get_drink_ingredient_name,
edit_ingredients,
};
},
});

View File

@ -0,0 +1,94 @@
<template>
<q-card>
<q-card-section>
<div class="text-h6">Getränk Bearbeiten</div>
</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="edit_drink.cost_per_volume"
class="col-xs-12 col-sm-6 q-pa-sm q-pb-lg"
filled
label="Preis pro L"
hint="Inkl. 19% Mehrwertsteuer"
suffix="€"
dense
/>
</div>
</q-card-section>
<q-card-section>
<drink-price-volumes v-model="edit_drink.volumes" editable />
</q-card-section>
<q-card-actions class="justify-around">
<q-btn label="Abbrechen" @click="cancel" />
<q-btn label="Speichern" color="primary" @click="save" />
</q-card-actions>
</q-card>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, onBeforeMount } from 'vue';
import { Drink, DrinkPriceVolume } from '../store';
import DrinkPriceVolumes from './CalculationTable/DrinkPriceVolumes.vue';
import { calc_volume } from 'src/plugins/pricelist/utils/utils';
export default defineComponent({
name: 'DrinkModify',
components: { DrinkPriceVolumes },
props: {
drink: {
type: Object as PropType<Drink>,
required: true,
},
},
emits: { save: () => true, cancel: () => true },
setup(props, { emit }) {
onBeforeMount(() => {
edit_drink.value = Object.assign({}, props.drink);
});
const edit_drink = ref<Drink>();
function save() {
emit('save');
}
function cancel() {
emit('cancel');
}
function updateVolume(volume: DrinkPriceVolume) {
if (volume) {
console.log('edit_volume', volume);
volume.volume = calc_volume(volume);
}
}
return { edit_drink, save, cancel, updateVolume };
},
});
</script>

View File

@ -27,7 +27,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
import { defineComponent, onBeforeMount, ref } from 'vue';
import Ingredients from 'src/plugins/pricelist/components/CalculationTable/Ingredients.vue';
import { DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/store';
export default defineComponent({
@ -39,7 +39,7 @@ export default defineComponent({
store.min_prices.forEach((min_price) => {
(<DrinkPriceVolume>volume.value).min_prices.push({
percentage: min_price,
price: computed<number>(() => {
price: /*computed<number>(() => {
let retVal = 0;
let extraIngredientPrice = 0;
volume.value.ingredients.forEach((ingredient) => {
@ -55,7 +55,7 @@ export default defineComponent({
}
});
return (retVal * min_price) / 100 + extraIngredientPrice;
}),
}) */ 0,
});
});
});

View File

@ -1,21 +1,20 @@
import { api } from 'src/boot/axios';
import { defineStore } from 'pinia';
import { AxiosResponse } from 'axios';
import { computed, ComputedRef, WritableComputedRef } from 'vue';
import {
calc_volume,
calc_cost_per_volume,
calc_all_min_prices,
} from 'src/plugins/pricelist/utils/utils';
interface MinPrice extends Omit<FG.MinPrices, 'price'> {
price?: WritableComputedRef<number>;
}
interface DrinkPriceVolume extends Omit<Omit<FG.DrinkPriceVolume, 'volume'>, 'min_prices'> {
interface DrinkPriceVolume extends Omit<FG.DrinkPriceVolume, 'volume'> {
_volume: number;
volume?: WritableComputedRef<number>;
min_prices: MinPrice[];
volume?: number;
}
interface Drink extends Omit<Omit<FG.Drink, 'cost_per_volume'>, 'volumes'> {
volumes: DrinkPriceVolume[];
cost_per_volume: WritableComputedRef<number | undefined>;
cost_per_volume?: number;
_cost_per_volume?: number;
}
@ -26,23 +25,7 @@ class DrinkPriceVolume implements DrinkPriceVolume {
this.prices = prices;
this.ingredients = ingredients;
this.min_prices = [];
this.volume = computed<number>({
get: () => {
if (this.ingredients.some((ingredient) => !!ingredient.drink_ingredient)) {
let retVal = 0;
this.ingredients.forEach((ingredient) => {
if (ingredient.drink_ingredient?.volume) {
retVal += ingredient.drink_ingredient.volume;
}
});
this._volume = retVal;
return retVal;
} else {
return this._volume;
}
},
set: (val) => (this._volume = val),
});
this.volume = calc_volume(this);
}
}
@ -67,18 +50,7 @@ class Drink {
this.volume = volume;
this.cost_per_package = cost_per_package;
this._cost_per_volume = cost_per_volume;
this.cost_per_volume = computed({
get: () => {
if (!!this.volume && !!this.package_size && !!this.cost_per_package) {
const retVal =
((this.cost_per_package || 0) / ((this.volume || 0) * (this.package_size || 0))) * 1.19;
this._cost_per_volume = Math.round(retVal * 1000) / 1000;
}
return this._cost_per_volume;
},
set: (val) => (this._cost_per_volume = val),
});
this.cost_per_volume = calc_cost_per_volume(this);
this.tags = tags;
this.type = type;
this.volumes = [];
@ -164,7 +136,7 @@ export const usePricelistStore = defineStore({
});
this.drinks.push(_drink);
});
this.create_min_prices();
calc_all_min_prices(this.drinks, this.min_prices);
console.log(this.drinks);
},
sortPrices(volume: DrinkPriceVolume) {
@ -217,7 +189,7 @@ export const usePricelistStore = defineStore({
_drink.volumes.push(_volume);
});
this.drinks.push(_drink);
this.create_min_prices();
calc_all_min_prices(this.drinks, this.min_prices);
},
async updateDrink(drink: Drink) {
console.log(drink);
@ -234,7 +206,7 @@ export const usePricelistStore = defineStore({
});
this.drinks[index] = _drink;
}
this.create_min_prices();
calc_all_min_prices(this.drinks, this.min_prices);
},
deleteDrink(drink: Drink) {
api
@ -271,46 +243,7 @@ export const usePricelistStore = defineStore({
},
async set_min_prices() {
await api.post<Array<number>>('pricelist/settings/min_prices', this.min_prices);
this.create_min_prices();
},
create_min_prices() {
this.drinks.forEach((drink) => {
drink.volumes.forEach((volume) => {
volume.min_prices = [];
this.min_prices.forEach((min_price) => {
let computedMinPrice: ComputedRef;
if (drink.cost_per_volume) {
computedMinPrice = computed<number>(
() =>
(<number>(<unknown>drink.cost_per_volume) *
<number>(<unknown>volume.volume) *
min_price) /
100
);
} else {
computedMinPrice = computed<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;
});
}
volume.min_prices.push({ percentage: min_price, price: computedMinPrice });
});
});
});
calc_all_min_prices(this.drinks, this.min_prices);
},
async upload_drink_picture(drink: Drink, file: File) {
const formData = new FormData();
@ -351,4 +284,4 @@ export const usePricelistStore = defineStore({
},
});
export { DrinkPriceVolume, MinPrice, Drink };
export { DrinkPriceVolume, Drink };

View File

@ -0,0 +1,71 @@
import { Drink, DrinkPriceVolume, usePricelistStore } from 'src/plugins/pricelist/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 = [];
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;
}
export { calc_volume, calc_cost_per_volume, calc_all_min_prices, calc_min_prices };