[pricelist] with backend, with errors

price can be add and deleted
This commit is contained in:
Tim Gröger 2021-03-15 19:57:42 +01:00
parent 724ae66dd7
commit f6951bdf0b
3 changed files with 373 additions and 307 deletions

View File

@ -13,10 +13,10 @@ declare namespace FG {
firstname: string; firstname: string;
lastname: string; lastname: string;
mail: string; mail: string;
birthday?: any; birthday?: Date;
roles: Array<string>; roles: Array<string>;
permissions?: any; permissions?: string[];
avatar_url?: any; avatar_url?: string;
} }
type Permission = string; type Permission = string;
interface Role { interface Role {
@ -29,41 +29,61 @@ declare namespace FG {
time: Date; time: Date;
amount: number; amount: number;
reversal_id: number; reversal_id: number;
sender_id?: any; sender_id?: number;
receiver_id?: any; receiver_id?: number;
author_id?: any; author_id?: number;
original_id?: any; original_id?: number;
} }
interface Drink { interface Drink {
id: number; id: number;
article_id?: string;
package_size?: number;
name: string; name: string;
volume?: number;
cost_price_pro_volume?: number;
cost_price_package_netto?: number;
tags: Array<Tag>;
type: DrinkType;
volumes: DrinkPriceVolume[];
}
interface DrinkIngredient {
id: number;
volume: number; volume: number;
cost_price: number; drink_ingredient_id: number;
discount: number; drink_ingredient?: Drink;
extra_charge?: any; price: number;
prices: Array<DrinkPrice>;
ingredients: Array<Ingredient>;
tags: Array<any>;
} }
interface DrinkPrice { interface DrinkPrice {
id: number; id: number;
volume: number;
price: number; price: number;
no_auto: boolean;
public: boolean; public: boolean;
description?: any; description?: string;
round_step: number; }
interface DrinkMinPrice {
percentage: number;
price: number;
}
interface DrinkPriceVolume {
id: number;
volume: number;
min_prices: DrinkMinPrice[];
prices: Array<DrinkPrice>;
ingredients: Array<DrinkIngredient & ExtraIngredient>;
} }
interface DrinkType { interface DrinkType {
id: number; id: number;
name: string; name: string;
} }
interface ExtraIngredient {
id: number;
name: string;
price: number;
}
interface Ingredient { interface Ingredient {
id: number; id: number;
volume: number; volume_id: number;
drink_parent_id: number; drink_ingredient: DrinkIngredient | null;
drink_ingredient_id: number; extra_ingredient: ExtraIngredient | null;
drink_ingredient?: any;
} }
interface Tag { interface Tag {
id: number; id: number;
@ -73,7 +93,7 @@ declare namespace FG {
id: number; id: number;
start: Date; start: Date;
end: Date; end: Date;
description?: any; description?: string;
type: EventType; type: EventType;
jobs: Array<Job>; jobs: Array<Job>;
} }
@ -84,7 +104,7 @@ declare namespace FG {
interface Job { interface Job {
id: number; id: number;
start: Date; start: Date;
end?: any; end?: Date;
comment: string; comment: string;
type: JobType; type: JobType;
services: Array<Service>; services: Array<Service>;

View File

@ -3,7 +3,7 @@
<q-table <q-table
title="Kalkulationstabelle" title="Kalkulationstabelle"
:columns="columns" :columns="columns"
:data="test" :data="drinks"
:visible-columns="visibleColumn" :visible-columns="visibleColumn"
:dense="$q.screen.lt.md" :dense="$q.screen.lt.md"
> >
@ -22,10 +22,10 @@
options-cover options-cover
/> />
</template> </template>
<template v-slot:body-cell-price_calc="price_calc"> <template v-slot:body-cell-volumes="volumes">
<q-table <q-table
:columns="column_calc" :columns="column_calc"
:data="price_calc.value" :data="volumes.value"
dense dense
:visible-columns="visibleColumn" :visible-columns="visibleColumn"
row-key="id" row-key="id"
@ -34,8 +34,8 @@
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width /> <q-th auto-width />
<q-th v-for="(col, index) in props.cols" :key="col + index" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col }} {{ col.label }}
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
@ -62,7 +62,7 @@
<div class="col"> <div class="col">
<q-badge color="primary">{{ min_price.percentage }}%</q-badge> <q-badge color="primary">{{ min_price.percentage }}%</q-badge>
</div> </div>
<div class="col" style="text-align: end;"> <div class="col" style="text-align: end">
{{ parseFloat(min_price.price).toFixed(3) }} {{ parseFloat(min_price.price).toFixed(3) }}
</div> </div>
</div> </div>
@ -83,30 +83,67 @@
:key="col.name" :key="col.name"
:props="prices_props" :props="prices_props"
> >
<div v-if="col.name == 'public'" class="full-width">
<q-toggle
v-model="col.value"
dense
@input="updatePrice(prices_props.row, props.row)"
/>
</div>
<div v-else class="full-width">
{{ col.value }} {{ col.value }}
</div>
</q-td> </q-td>
<q-td> <q-td>
<q-btn <q-btn
color="negative" color="negative"
padding="xs"
round round
size="xs" size="xs"
icon="mdi-delete" icon="mdi-delete"
@click="deletePrice(prices_props.row)" @click="deletePrice(prices_props.row, props.row)"
v-if="!prices_props.row.to_delete"
/> />
<q-btn color="positive" size="xs" label="Speichern" v-else />
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
<template v-slot:bottom> <template v-slot:bottom>
<div class="full-width row justify-end"> <div class="full-width row justify-end">
<q-btn <q-btn size="xs" icon-right="add" color="positive" label="Preis hinzufügen">
size="xs" <q-menu anchor="center middle" self="center middle">
icon-right="add" <div class="row justify-around q-pa-sm">
color="positive" <q-input
label="Preis hinzufügen" v-model.number="newPrice.price"
@click="addPrice(col.value)" dense
filled
class="q-px-sm"
type="number"
label="Preis"
/> />
<q-input
v-model="newPrice.description"
dense
filled
class="q-px-sm"
label="Beschreibung"
/>
<q-toggle
v-model="newPrice.public"
dense
class="q-px-sm"
label="Öffentlich"
/>
</div>
<div class="row justify-between q-pa-sm">
<q-btn label="Abbrechen" @click="cancelAddPrice" v-close-popup />
<q-btn
label="Speichern"
color="primary"
@click="addPrice(props.row)"
v-close-popup
/>
</div>
</q-menu>
</q-btn>
</div> </div>
</template> </template>
<template v-slot:no-data class="justify-end"> <template v-slot:no-data class="justify-end">
@ -130,18 +167,22 @@
<q-tr v-show="props.expand" :props="props"> <q-tr v-show="props.expand" :props="props">
<q-td colspan="100%"> <q-td colspan="100%">
<div <div
v-for="(ingredient, index) in props.row.ingredients" v-for="ingredient in props.row.ingredients"
:key="`${props.key}_${index}`" :key="`volume:${props.row.id},ingredient:${ingredient.id}`"
class="full-width row justify-evenly q-py-xs" class="full-width row justify-evenly q-py-xs"
>
<div
class="full-width row justify-evenly q-py-xs"
v-if="ingredient.drink_ingredient"
> >
<q-select <q-select
class="col q-px-sm" class="col q-px-sm"
label="Getränk" label="Getränk"
filled filled
dense dense
:options="test" :options="drinks"
option-label="name" option-label="name"
v-model="ingredient.drink" v-model="ingredient.drink_ingredient"
@input="calc_min_prices(props.row)" @input="calc_min_prices(props.row)"
/> />
<q-input <q-input
@ -150,19 +191,23 @@
type="number" type="number"
filled filled
dense dense
v-model="ingredient.volume" v-model.number="ingredient.volume"
@change="calc_min_prices(props.row)" @change="calc_min_prices(props.row)"
step="0.01" step="0.01"
min="0" min="0"
/> />
</div> </div>
<div v-else-if="ingredient.name">
{{ ingredient.name }} {{ ingredient.price }}
</div>
</div>
<div class="full-width row justify-end q-py-xs"> <div class="full-width row justify-end q-py-xs">
<q-btn <q-btn
size="sm" size="sm"
icon-right="add" icon-right="add"
color="positive" color="positive"
label="Zutat hinzufügen" label="Zutat hinzufügen"
@click="addIngredient(props.row.ingredients)" @click="addIngredient(props.row.ingredients, props.row.id)"
@change="calc_min_prices(props.row)" @change="calc_min_prices(props.row)"
/> />
</div> </div>
@ -176,7 +221,7 @@
icon-right="add" icon-right="add"
label="Abgabe hinzufügen" label="Abgabe hinzufügen"
size="xs" size="xs"
@click="addVolume(price_calc.value)" @click="addVolume(volumes.value)"
/> />
</div> </div>
</template> </template>
@ -187,280 +232,181 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onBeforeMount, ref } from '@vue/composition-api'; import { defineComponent, onBeforeMount, ref, computed } from '@vue/composition-api';
import { Store } from 'vuex'; import store, { calc_min_prices } from '../store/altStore';
import { StateInterface } from '../../../store';
import { DrinkInterface } from '../store/drinks';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
export default defineComponent({ export default defineComponent({
name: 'CalculationTable', name: 'CalculationTable',
setup(_, { root }) { setup(_, { root }) {
const store = <Store<StateInterface>>root.$store;
const state = <DrinkInterface>store.state.drink;
const drinks = ref();
onBeforeMount(() => { onBeforeMount(() => {
void store store.actions.getDrinks();
.dispatch('drink/getDrinks')
.then(() => (drinks.value = drinks.value = state.drinks));
}); });
const columns = [ const columns = [
{ {
name: 'name', name: 'name',
label: 'Getränkename', label: 'Getränkename',
field: 'name' field: 'name',
}, },
{ {
name: 'article_id', name: 'article_id',
label: 'Artikelnummer', label: 'Artikelnummer',
field: 'article_id' field: 'article_id',
}, },
{ {
name: 'drink_kind', name: 'drink_type',
label: 'Kategorie', label: 'Kategorie',
field: 'drink_kind' field: 'type',
format: (val: FG.DrinkType) => `${val.name}`,
}, },
{ {
name: 'volume_package', name: 'volume_package',
label: 'Inhalt in l des Gebinde', label: 'Inhalt in l des Gebinde',
field: 'volume_package' field: 'volume',
}, },
{ {
name: 'package_size', name: 'package_size',
label: 'Gebindegröße', label: 'Gebindegröße',
field: 'package_size' field: 'package_size',
}, },
{ {
name: 'cost_price_package_netto', name: 'cost_price_package_netto',
label: 'Preis Netto/Gebinde', label: 'Preis Netto/Gebinde',
field: 'cost_price_package_netto', field: 'cost_price_package_netto',
format: (val: number) => `${val.toFixed(3)}` format: (val: number | null) => (val ? `${val.toFixed(3)}` : ''),
}, },
{ {
name: 'cost_price_pro_volume', name: 'cost_price_pro_volume',
label: 'Preis mit 19%/Liter', label: 'Preis mit 19%/Liter',
field: 'cost_price_pro_volume', field: 'cost_price_pro_volume',
format: (val: number) => `${val.toFixed(3)}` format: (val: number | null) => (val ? `${val.toFixed(3)}` : ''),
}, },
{ {
name: 'price_calc', name: 'volumes',
label: 'Preiskalkulation', label: 'Preiskalkulation',
field: 'price_calc' field: 'volumes',
} },
]; ];
const column_calc = [ const column_calc = [
{ {
name: 'volume', name: 'volume',
label: 'Abgabe in l', label: 'Abgabe in l',
field: 'volume', field: 'volume',
format: (val: number) => `${val} L` format: (val: number) => `${val} L`,
}, },
{ {
name: 'min_prices', name: 'min_prices',
label: 'Minimal Preise', label: 'Minimal Preise',
field: 'min_prices' field: 'min_prices',
}, },
{ {
name: 'prices', name: 'prices',
label: 'Preise', label: 'Preise',
field: 'prices' field: 'prices',
} },
]; ];
const column_prices = [ const column_prices = [
{ {
name: 'price', name: 'price',
label: 'Preis', label: 'Preis',
field: 'price', field: 'price',
format: (val: number) => `${val.toFixed(2)}` format: (val: number) => `${val.toFixed(2)}`,
}, },
{ {
name: 'description', name: 'description',
label: 'Beschreibung', label: 'Beschreibung',
field: 'description' field: 'description',
} },
{
name: 'public',
label: 'Öffentlich',
field: 'public',
},
]; ];
const test = ref([
{
name: 'Elbhang Rot',
article_id: 41807,
drink_kind: 'Bier',
volume_package: 50,
package_size: 1,
cost_price_package_netto: 97,
cost_price_pro_volume: 2.309,
price_calc: [
{
id: v4(),
volume: 0.5,
min_prices: [
{
percentage: 100,
price: 1.15
},
{
percentage: 250,
price: 2.875
},
{
percentage: 300,
price: 3.45
}
],
prices: [
{ price: 2, description: '', to_delete: false },
{ price: 1.4, description: 'Club intern', to_delete: false },
{
price: 1.6,
description: 'Club extern',
to_delete: false
}
]
}
]
},
{
name: 'Sinalco Cola',
article_id: 443,
drink_kind: 'AFG',
volume_package: 1,
package_size: 12,
cost_price_package_netto: 7.28,
cost_price_pro_volume: 0.722,
price_calc: [
{
id: v4(),
volume: 0.2,
min_prices: [
{
percentage: 100,
price: 0.14
},
{
percentage: 250,
price: 0.35
},
{
percentage: 300,
price: 0.42
}
],
prices: [
{
price: 1,
description: 'klein',
to_delete: false
},
{ price: 0.5, description: 'klein club intern', to_delete: false }
]
},
{
id: v4(),
volume: 0.4,
min_prices: [
{
percentage: 100,
price: 0.289
},
{
percentage: 250,
price: 0.722
},
{
percentage: 300,
price: 0.866
}
],
prices: [
{
price: 1.8,
description: 'groß',
to_delet: false
},
{ price: 1, description: 'groß club intern', to_delete: false }
]
}
]
}
]);
const visibleColumn = ref([ const visibleColumn = ref([
'name', 'name',
'drink_kind', 'drink_kind',
'cost_price_pro_volumne', 'cost_price_pro_volumne',
'price_calc', 'volumes',
'volume', 'volume',
'min_prices', 'min_prices',
'prices', 'prices',
'price', 'price',
'description' 'description',
'public',
]); ]);
function deletePrice(row) { function deletePrice(row: FG.DrinkPrice) {
console.log(row); console.log(row);
row.to_delete = true;
} }
function addPrice(table) { const emptyPrice = {
console.log(table); price: 0,
table.push({ description: '',
price: 20, public: true,
description: 'test', };
to_delete: false const newPrice = ref(emptyPrice);
}); function addPrice(volume: FG.DrinkPriceVolume) {
store.actions.setPrice({ ...newPrice.value }, volume);
cancelAddPrice();
}
function cancelAddPrice() {
setTimeout(() => {
addPrice.value = emptyPrice;
}, 200);
}
function updatePrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
store.actions.updatePrice(price, volume);
}
function deletePrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
console.log(price, volume);
store.actions.deletePrice(price, volume);
} }
function addVolume(table) { function addVolume(table: FG.DrinkPriceVolume[]) {
table.push({ table.push({
id: v4(), id: v4(),
volume: null, volume: null,
min_prices: [ min_prices: [
{ {
percentage: 100, percentage: 100,
price: null price: 0,
}, },
{ {
percentage: 250, percentage: 250,
price: null price: 0,
}, },
{ {
percentage: 300, percentage: 300,
price: null price: 0,
} },
], ],
prices: [], prices: [],
ingredients: [] ingredients: [],
}); });
} }
function addIngredient(ingredients) { function addIngredient(ingredients: FG.Ingredient[]) {
ingredients.push({ drink: null, volume: null }); ingredients.push({ id: -1, volume_id: 0, drink_ingredient: null, extra_ingredient: null });
}
function calc_min_prices(row) {
console.log(row.ingredients);
row.volume = 0;
let cost_price = 0;
row.ingredients.forEach(ingredient => {
row.volume = row.volume + Number.parseFloat(ingredient.volume);
cost_price = Number.parseFloat(ingredient.volume) * ingredient.drink.cost_price_pro_volume;
});
console.log(row.volume, cost_price);
row.min_prices.forEach(min_price => {
min_price.price = (cost_price * min_price.percentage) / 100;
});
} }
return { return {
drinks, drinks: computed({ get: () => store.state.drinks, set: (val) => console.log(val) }),
columns, columns,
test,
column_calc, column_calc,
column_prices, column_prices,
visibleColumn, visibleColumn,
deletePrice, deletePrice,
newPrice,
addPrice, addPrice,
updatePrice,
deletePrice,
cancelAddPrice,
addVolume, addVolume,
addIngredient, addIngredient,
calc_min_prices calc_min_prices,
console,
}; };
} },
}); });
</script> </script>

View File

@ -0,0 +1,100 @@
import { reactive } from '@vue/composition-api';
import { axios } from 'src/boot/axios';
import { AxiosResponse } from 'axios';
import ExtraIngredient = FG.ExtraIngredient;
const state = reactive<{ drinks: FG.Drink[]; tags: FG.Tag[]; drinkTypes: FG.DrinkType[] }>({
drinks: [],
tags: [],
drinkTypes: [],
});
const actions = {
getDrinks() {
axios
.get('pricelist/drinks')
.then((response: AxiosResponse<FG.Drink[]>) => {
state.drinks = response.data;
console.log(state.drinks);
state.drinks.forEach((drink: FG.Drink) => {
drink.volumes.forEach((volume: FG.DrinkPriceVolume) => {
volume.min_prices = [
{ percentage: 100, price: (drink.cost_price_pro_volume || 0) * volume.volume * 1 },
{ percentage: 250, price: (drink.cost_price_pro_volume || 0) * volume.volume * 2.5 },
{ percentage: 300, price: (drink.cost_price_pro_volume || 0) * volume.volume * 3 },
];
if (volume.ingredients.length > 0) {
calc_min_prices(volume);
}
this.sortPrices(volume);
});
});
})
.catch((err) => console.warn(err));
},
setPrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
axios
.post(`pricelist/prices/volumes/${volume.id}`, price)
.then((response: AxiosResponse<FG.DrinkPrice>) => {
volume.prices.push(response.data);
this.sortPrices(volume);
})
.catch((err) => console.warn(err));
},
sortPrices(volume: FG.DrinkPriceVolume) {
volume.prices.sort((a, b) => {
if (a.price > b.price) return 1;
if (b.price > a.price) return -1;
return 0;
});
},
deletePrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
axios
.delete(`pricelist/prices/${price.id}`)
.then(() => {
const index = volume.prices.findIndex((a) => a.id == price.id);
if (index > -1) {
volume.prices.splice(index, 1);
}
})
.catch((err) => console.warn(err));
},
updatePrice(price: FG.DrinkPrice, volume: FG.DrinkPriceVolume) {
axios
.put(`pricelist/prices/${price.id}`, price)
.then((response: AxiosResponse<FG.DrinkPrice>) => {
const index = volume.prices.findIndex((a) => a.id === price.id);
if (index > -1) {
volume.prices[index] = response.data;
this.sortPrices(volume);
}
})
.catch((err) => console.log(err));
},
};
const getters = {};
function calc_min_prices(row: FG.DrinkPriceVolume) {
row.volume = 0;
let cost_price = 0;
row.ingredients.forEach((ingredient: FG.DrinkIngredient & FG.ExtraIngredient) => {
console.log(ingredient);
if (ingredient.drink_ingredient) {
row.volume = row.volume + ingredient.volume;
cost_price += ingredient.volume * (ingredient.drink_ingredient.cost_price_pro_volume || 0);
} else if (ingredient.name && ingredient.price) {
cost_price += ingredient.price;
}
});
row.min_prices.forEach((min_price) => {
min_price.price = (cost_price * min_price.percentage) / 100;
});
}
export { calc_min_prices };
export default {
state,
actions,
getters,
};