diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e63349 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "private": true, + "license": "MIT", + "version": "1.0.0-alpha.1", + "name": "@flaschengeist/pricelist", + "author": "Tim Gröger ", + "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" + } +} diff --git a/src/components/BuildManual/BuildManualVolume.vue b/src/components/BuildManual/BuildManualVolume.vue new file mode 100644 index 0000000..4400095 --- /dev/null +++ b/src/components/BuildManual/BuildManualVolume.vue @@ -0,0 +1,63 @@ + + diff --git a/src/components/BuildManual/BuildManualVolumePart.vue b/src/components/BuildManual/BuildManualVolumePart.vue new file mode 100644 index 0000000..6f07523 --- /dev/null +++ b/src/components/BuildManual/BuildManualVolumePart.vue @@ -0,0 +1,66 @@ + + diff --git a/src/components/CalculationTable.vue b/src/components/CalculationTable.vue new file mode 100644 index 0000000..dabe8a2 --- /dev/null +++ b/src/components/CalculationTable.vue @@ -0,0 +1,511 @@ + + + + + diff --git a/src/components/CalculationTable/BuildManual.vue b/src/components/CalculationTable/BuildManual.vue new file mode 100644 index 0000000..cabbdea --- /dev/null +++ b/src/components/CalculationTable/BuildManual.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/CalculationTable/DrinkPriceVolumes.vue b/src/components/CalculationTable/DrinkPriceVolumes.vue new file mode 100644 index 0000000..7ad9aa2 --- /dev/null +++ b/src/components/CalculationTable/DrinkPriceVolumes.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/src/components/CalculationTable/Ingredients.vue b/src/components/CalculationTable/Ingredients.vue new file mode 100644 index 0000000..7090199 --- /dev/null +++ b/src/components/CalculationTable/Ingredients.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/src/components/CalculationTable/NewPrice.vue b/src/components/CalculationTable/NewPrice.vue new file mode 100644 index 0000000..9f26a70 --- /dev/null +++ b/src/components/CalculationTable/NewPrice.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/components/DrinkModify.vue b/src/components/DrinkModify.vue new file mode 100644 index 0000000..ef7565d --- /dev/null +++ b/src/components/DrinkModify.vue @@ -0,0 +1,353 @@ + + diff --git a/src/components/DrinkTypes.vue b/src/components/DrinkTypes.vue new file mode 100644 index 0000000..517e4c6 --- /dev/null +++ b/src/components/DrinkTypes.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/components/ExtraIngredients.vue b/src/components/ExtraIngredients.vue new file mode 100644 index 0000000..096d5ea --- /dev/null +++ b/src/components/ExtraIngredients.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/components/MinPriceSetting.vue b/src/components/MinPriceSetting.vue new file mode 100644 index 0000000..f736bfa --- /dev/null +++ b/src/components/MinPriceSetting.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/Pricelist.vue b/src/components/Pricelist.vue new file mode 100644 index 0000000..3660b58 --- /dev/null +++ b/src/components/Pricelist.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/src/components/SearchInput.vue b/src/components/SearchInput.vue new file mode 100644 index 0000000..c7d38a3 --- /dev/null +++ b/src/components/SearchInput.vue @@ -0,0 +1,90 @@ + + +a diff --git a/src/components/Tags.vue b/src/components/Tags.vue new file mode 100644 index 0000000..a690550 --- /dev/null +++ b/src/components/Tags.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/pages/CocktailBuilder.vue b/src/pages/CocktailBuilder.vue new file mode 100644 index 0000000..1b02dd0 --- /dev/null +++ b/src/pages/CocktailBuilder.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/src/pages/InnerPricelist.vue b/src/pages/InnerPricelist.vue new file mode 100644 index 0000000..9e23e80 --- /dev/null +++ b/src/pages/InnerPricelist.vue @@ -0,0 +1,39 @@ + + diff --git a/src/pages/OuterPricelist.vue b/src/pages/OuterPricelist.vue new file mode 100644 index 0000000..338f9ae --- /dev/null +++ b/src/pages/OuterPricelist.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/pages/Receipts.vue b/src/pages/Receipts.vue new file mode 100644 index 0000000..1c94485 --- /dev/null +++ b/src/pages/Receipts.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue new file mode 100644 index 0000000..6f51be7 --- /dev/null +++ b/src/pages/Settings.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 0000000..a378a74 --- /dev/null +++ b/src/permissions.ts @@ -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', +}; diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..1a65597 --- /dev/null +++ b/src/plugin.ts @@ -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; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..cde6fdd --- /dev/null +++ b/src/routes/index.ts @@ -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'), + }, + }, +]; diff --git a/src/shims.d.ts b/src/shims.d.ts new file mode 100644 index 0000000..e051226 --- /dev/null +++ b/src/shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { ComponentOptions } from 'vue' + const component: ComponentOptions + export default component +} \ No newline at end of file diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..4707cdb --- /dev/null +++ b/src/store.ts @@ -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 { + _volume: number; + volume?: number; +} + +interface Drink extends Omit, 'volumes'> { + volumes: DrinkPriceVolume[]; + cost_per_volume?: number; + _cost_per_volume?: number; +} + +interface Pricelist { + name: string; + type: FG.DrinkType; + tags: Array; + 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, + drinks: [] as Array, + extraIngredients: [] as Array, + min_prices: [] as Array, + tags: [] as Array, + pricecalc_columns: [] as Array, + pricelist_view: false as boolean, + pricelist_columns_order: [] as Array, + }), + + actions: { + async getDrinkTypes(force = false) { + if (force || this.drinks.length == 0) { + const { data } = await api.get>('/pricelist/drink-types'); + this.drinkTypes = data; + } + return this.drinkTypes; + }, + async addDrinkType(name: string) { + const { data } = await api.post('/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>( + 'pricelist/ingredients/extraIngredients' + ); + this.extraIngredients = data; + }, + async setExtraIngredient(ingredient: FG.ExtraIngredient) { + const { data } = await api.post( + 'pricelist/ingredients/extraIngredients', + ingredient + ); + this.extraIngredients.push(data); + }, + async updateExtraIngredient(ingredient: FG.ExtraIngredient) { + const { data } = await api.put( + `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>('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('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(`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>('pricelist/settings/min_prices'); + this.min_prices = data; + }, + async set_min_prices() { + await api.post>('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(`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>('/pricelist/tags'); + this.tags = data; + }, + async setTag(tag: FG.Tag) { + const { data } = await api.post('/pricelist/tags', tag); + this.tags.push(data); + }, + async updateTag(tag: FG.Tag) { + const { data } = await api.put(`/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>(`pricelist/users/${userid}/pricecalc_columns`); + this.pricecalc_columns = data; + }, + async updatePriceCalcColumn(userid: string, data: Array) { + await api.put>(`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>(`pricelist/users/${userid}/pricelist`, { value: data }); + this.pricelist_view = data; + }, + async getPriceListColumnOrder(userid: string) { + const { data } = await api.get>( + `pricelist/users/${userid}/pricecalc_columns_order` + ); + this.pricelist_columns_order = data; + }, + async updatePriceListColumnOrder(userid: string, data: Array) { + await api.put>(`pricelist/users/${userid}/pricecalc_columns_order`, data); + this.pricelist_columns_order = data; + }, + }, + getters: { + pricelist() { + const retVal: Array = []; + this.drinks.forEach((drink) => { + drink.volumes.forEach((volume) => { + volume.prices.forEach((price) => { + retVal.push({ + name: drink.name, + type: drink.type, + tags: >drink.tags, + volume: volume.volume, + price: price.price, + public: price.public, + description: price.description, + }); + }); + }); + }); + return retVal; + }, + }, +}); + +export { DrinkPriceVolume, Drink, Order }; diff --git a/src/utils/filter.ts b/src/utils/filter.ts new file mode 100644 index 0000000..cb8c5ba --- /dev/null +++ b/src/utils/filter.ts @@ -0,0 +1,39 @@ +import { Drink } from '../store'; + +function filter( + rows: Array, + terms: Search, + cols: Array, + 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 }; diff --git a/src/utils/sort.ts b/src/utils/sort.ts new file mode 100644 index 0000000..cfd6f2a --- /dev/null +++ b/src/utils/sort.ts @@ -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 }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..c31b13a --- /dev/null +++ b/src/utils/utils.ts @@ -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, min_prices: Array) { + 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 * (_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 +) { + const retVal: Array = []; + volume.min_prices = []; + if (min_prices) { + min_prices.forEach((min_price) => { + let computedMinPrice: number; + if (cost_per_volume) { + computedMinPrice = (cost_per_volume * volume.volume * min_price) / 100; + } else { + computedMinPrice = helper(volume, min_price); + } + retVal.push({ percentage: min_price, price: computedMinPrice }); + }); + } + return retVal; +} + +function clone(o: T): T { + return JSON.parse(JSON.stringify(o)); +} + +interface DeleteObjects { + prices: Array; + volumes: Array; + ingredients: Array; +} +export { DeleteObjects }; + +export { calc_volume, calc_cost_per_volume, calc_all_min_prices, calc_min_prices, clone };